commit for day 1
This commit is contained in:
293
module-a/module-a-kr.md
Normal file
293
module-a/module-a-kr.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# ⚡ Test Project: Module A — Speed Test
|
||||
|
||||
## 1. 과제 개요 (Project Overview)
|
||||
|
||||
본 과제는 웹 기술 전반에 걸친 실무 숙련도를 빠르고 정확하게 측정하는 **Speed Test** 형식의 평가입니다. 선수는 **A(Design)**, **B(Layout)**, **C(Frontend)**, **D(Backend)** 4개 분류에 걸쳐 총 20개의 독립 Task를 제한 시간 내에 완성해야 합니다. 각 Task는 서로 독립적이며, 순서에 상관없이 자유롭게 풀이할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 기술 스택 및 제약 조건 (Tech Stack & Constraints)
|
||||
|
||||
| 분류 | 허용 기술 | 제약 사항 |
|
||||
| :--- | :--- | :--- |
|
||||
| **A. Design** | GIMP | Photoshop 등 타 편집 프로그램 사용 불가 |
|
||||
| **B. Layout** | HTML, CSS | JavaScript 사용 불가 |
|
||||
| **C. Frontend** | JavaScript (Vanilla) | 외부 라이브러리/프레임워크 사용 불가 |
|
||||
| **D. Backend** | PHP, MySQL | PDO 방식으로 DB 연결 |
|
||||
|
||||
* 모든 Task의 결과물은 데스크톱 Chrome 브라우저 기준으로 평가합니다.
|
||||
* 각 Task의 결과물은 `/module_a/{task_id}/` 경로에 저장하십시오. (예: `/module_a/a1/`, `/module_a/b1/`)
|
||||
* Design Task 제출 시에는 GIMP 원본 파일(`.xcf`)도 함께 포함해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Task 목록 및 요구사항 (Tasks)
|
||||
|
||||
### 🎨 A. Design — GIMP 활용 능력
|
||||
|
||||
> **제공 파일**: A1(`source.png`), A2(`base.jpg`), A3(`photo.jpg`, `mask.png`) Task의 작업 디렉토리에 미리 배치되어 있습니다. A4, A5는 제공 파일이 없으며 선수가 직접 생성합니다.
|
||||
|
||||
#### **A1. 레이어 합성 및 텍스트 배치** `Easy`
|
||||
|
||||
GIMP에서 레이어 구조를 활용한 이미지를 제작하십시오.
|
||||
|
||||
* 캔버스 크기: **800×400px**, 해상도: **72dpi**
|
||||
* **레이어 1**: `#2C3E50` 단색 배경을 채우십시오.
|
||||
* **레이어 2**: 제공된 이미지(`source.png`)를 불러와 캔버스 **중앙**에 배치하고 불투명도를 **70%**로 설정하십시오.
|
||||
* **레이어 3**: 텍스트 **"Hello, GIMP"** 를 흰색(`#FFFFFF`), 36pt로 추가하고 캔버스 좌측 상단 **(x: 20px, y: 20px)** 에 배치하십시오.
|
||||
* 모든 레이어를 병합(Flatten Image)하기 전에 `.xcf` 원본 파일로 먼저 저장하십시오. 이후 레이어를 병합하여 `result.png`로 내보내십시오.
|
||||
|
||||
#### **A2. 선택 영역 및 레이어 합성** `Easy`
|
||||
|
||||
GIMP의 선택 도구와 레이어를 활용하여 이미지에 도형 효과를 적용하십시오.
|
||||
|
||||
* 제공된 이미지(`base.jpg`)를 배경 레이어로 여십시오.
|
||||
* **효과 1**: 새 레이어를 추가하고 이미지 좌측 상단 **(0, 0) ~ (200, 200)** 영역을 사각형 선택 도구로 선택한 뒤 `#E74C3C`으로 채우십시오. 이 레이어의 불투명도를 **60%**로 설정하십시오.
|
||||
* **효과 2**: 또 다른 새 레이어를 추가하고 이미지 **중앙**에 지름 **150px** 타원 선택 영역을 만드십시오. `Selection → Border` (한국어: 선택 → 경계선)에서 경계 두께를 **3px**로 설정한 뒤 `#FFFFFF`로 채우십시오.
|
||||
* `.xcf` 원본 파일로 먼저 저장한 뒤, 레이어를 병합(Flatten Image)하여 `result.jpg`로 내보내십시오.
|
||||
|
||||
#### **A3. 색상 보정 및 레이어 마스크** `Normal`
|
||||
|
||||
제공된 이미지(`photo.jpg`)를 GIMP에서 색상 보정하고 레이어 마스크를 적용하십시오.
|
||||
|
||||
* **색상 보정**: 색조-채도(Hue-Saturation) 도구로 채도(Saturation)를 **+40** 올리십시오.
|
||||
* **곡선 조정**: 곡선(Curves) 도구에서 밝은 영역 출력값을 **+20 이상**, 어두운 영역 출력값을 **-20 이하**로 조정하여 대비를 높이십시오.
|
||||
* **레이어 마스크**: 이미지 레이어에 레이어 마스크를 추가하고, 제공된 마스크 이미지(`mask.png`)를 해당 마스크 레이어에 붙여넣어 피사체 외 배경이 제거되도록 하십시오.
|
||||
* 완성본을 `result.png`로 내보내기 전에 `.xcf` 원본 파일로 먼저 저장하십시오. 내보낼 때 투명 배경이 유지되도록 PNG 형식을 사용하십시오.
|
||||
|
||||
#### **A4. 텍스트 이펙트 및 발광 효과** `Normal`
|
||||
|
||||
GIMP에서 텍스트에 발광(glow) 시각 효과를 적용하십시오.
|
||||
|
||||
* 캔버스 크기: **600×200px**, 배경색: `#1A1A2E`
|
||||
* 텍스트 **"SPEED TEST"** 를 흰색(`#FFFFFF`), Bold, 60pt로 캔버스 중앙에 배치하십시오.
|
||||
* 텍스트 레이어를 복사하여 복사본 레이어를 텍스트 레이어 **아래**에 배치하십시오.
|
||||
* 복사본 레이어를 래스터화(Layer → Rasterize)한 뒤, 레이어 패널에서 해당 레이어의 모드(Mode)를 **`Color`**로 먼저 변경하십시오. 그 다음 전경색을 `#4A90D9`로 설정하고 `Edit → Fill with Foreground Color`를 실행하여 파란색이 적용되도록 하십시오.
|
||||
* 해당 복사본 레이어에 **가우시안 흐림(Gaussian Blur)** 효과를 반경 **8px**로 적용하여 외곽 발광 효과를 완성하십시오.
|
||||
* `.xcf` 원본 파일로 먼저 저장한 뒤 `result.png`로 내보내십시오.
|
||||
|
||||
#### **A5. 텍스트 워프 및 필터 효과** `Hard`
|
||||
|
||||
GIMP에서 텍스트를 래스터화하고 Distorts 필터를 조합하여 시각적 변형 효과를 구현하십시오.
|
||||
|
||||
* 캔버스 크기: **800×300px**, 배경색: `#0D0D0D`
|
||||
* 텍스트 **"DISTORTION"** 을 흰색(`#FFFFFF`), Bold, 72pt로 캔버스 중앙에 배치하십시오.
|
||||
* 텍스트 레이어를 래스터화(Layer → Rasterize)하십시오.
|
||||
* 래스터화된 텍스트 레이어에 다음 두 가지 Distorts 필터를 순서대로 적용하십시오.
|
||||
* **Ripple**: Amplitude **8**, Wavelength **40**, 방향 Horizontal
|
||||
* **Whirl and Pinch**: Whirl angle **20**도, Pinch **0**, Radius **1.0**
|
||||
* 필터 적용이 완료된 텍스트 레이어를 복사하여 복사본에 **가우시안 흐림(Gaussian Blur)** 반경 **3px**를 적용하고, 복사본을 원본 텍스트 레이어 아래에 배치하여 잔상 효과를 구현하십시오.
|
||||
* `.xcf` 원본 파일로 먼저 저장한 뒤 `result.png`로 내보내십시오.
|
||||
|
||||
---
|
||||
|
||||
### 📐 B. Layout — HTML / CSS 전용
|
||||
|
||||
#### **B1. Flexbox 카드 레이아웃** `Easy`
|
||||
|
||||
HTML과 CSS만을 사용하여 카드 목록 레이아웃을 구현하십시오.
|
||||
|
||||
* **Flexbox**를 사용하여 카드를 한 줄에 **4개**씩 배치하십시오. (`flex-wrap: wrap` 사용)
|
||||
* 각 카드는 이미지 영역(`<div>`, 높이 **160px**, 배경색 지정), 제목(`<h3>`), 본문 텍스트(`<p>`), 버튼(`<button>`)을 포함해야 합니다.
|
||||
* 카드 간 간격은 **24px**로 설정하십시오.
|
||||
* 더미 카드를 **8개** 작성하십시오. 각 카드의 이미지 영역 배경색은 서로 달라야 합니다.
|
||||
|
||||
#### **B2. Sticky 헤더 및 CSS 인터랙션** `Easy`
|
||||
|
||||
HTML과 CSS만을 사용하여 헤더와 CSS 전용 인터랙션을 구현하십시오.
|
||||
|
||||
* **Flexbox**를 사용하여 좌측에 로고 텍스트, 중앙에 메뉴 링크(4개), 우측에 버튼(2개)을 배치하십시오.
|
||||
* 메뉴 링크 호버 시 하단에 **2px 실선 언더라인**이 `transition`과 함께 나타나는 CSS 전용 인터랙션을 구현하십시오. (`border-bottom` 또는 `::after` pseudo-element 활용)
|
||||
* `position: sticky; top: 0;`을 사용하여 스크롤 시 헤더가 상단에 고정되도록 하십시오.
|
||||
* 헤더 아래에 최소 높이 **3000px** 이상의 더미 콘텐츠를 배치하여 스크롤이 가능하도록 하십시오.
|
||||
|
||||
#### **B3. CSS Grid 2단 레이아웃** `Normal`
|
||||
|
||||
HTML과 CSS만을 사용하여 CSS Grid 기반의 2단 페이지 레이아웃을 구현하십시오.
|
||||
|
||||
* **CSS Grid**를 사용하여 좌측 사이드바(**250px** 고정)와 우측 메인 콘텐츠 영역(`1fr`)으로 구성된 2단 레이아웃을 구현하십시오.
|
||||
* 좌측 사이드바에는 세로 내비게이션 메뉴(5개 항목)를 배치하십시오. 첫 번째 항목은 배경색과 텍스트 색상으로 활성 상태를 표시하십시오.
|
||||
* 우측 메인 영역에는 제목, 본문 텍스트, **3열 카드 그리드**(CSS Grid 사용, 카드 6개)를 순서대로 배치하십시오.
|
||||
* `height: 100vh`를 전체 레이아웃 래퍼에 적용하고 `overflow: hidden`을 설정하십시오. 사이드바와 메인 영역 각각에 `height: 100vh`와 `overflow-y: auto`를 적용하여 각 영역이 독립적으로 스크롤되도록 하십시오.
|
||||
|
||||
#### **B4. CSS 애니메이션 및 트랜지션** `Normal`
|
||||
|
||||
HTML과 CSS만을 사용하여 다양한 CSS 애니메이션과 트랜지션 효과를 하나의 페이지에 구현하십시오.
|
||||
|
||||
* **카드 호버 효과**: 카드 4개를 Flexbox로 가로 배치하고, 각 카드에 호버 시 위로 **8px** 이동하고 그림자가 강해지는 트랜지션 효과를 적용하십시오. (`transform: translateY(-8px)`, `box-shadow`, `transition` 활용)
|
||||
* **로딩 스피너**: `@keyframes`를 사용하여 원형 로딩 스피너를 구현하십시오. 스피너는 지름 **48px**, 테두리 두께 **4px**이며, 상단 테두리(`border-top`)만 `#3498DB` 색상으로, 나머지 테두리는 `#e0e0e0`으로 표시하고 무한 회전합니다.
|
||||
* **페이드인 텍스트**: 페이지 로드 시 제목 텍스트(`<h1>`)가 `translateY(20px)`에서 `translateY(0)`으로 이동하며 `opacity: 0`에서 `opacity: 1`로 나타나는 `@keyframes` 애니메이션을 구현하십시오. (`animation-duration: 0.8s`, `animation-fill-mode: both`)
|
||||
|
||||
#### **B5. CSS Grid 및 고급 선택자 활용** `Hard`
|
||||
|
||||
HTML과 CSS만을 사용하여 2단 콘텐츠 레이아웃과 CSS 전용 탭 인터랙션을 구현하십시오.
|
||||
|
||||
* **CSS Grid**를 사용하여 좌측(65%)과 우측(35%)으로 분할된 2단 레이아웃을 구현하십시오.
|
||||
* **좌측 영역**: 높이 **300px**의 메인 이미지 영역(`<div>`, 배경색 지정)과 하단 썸네일 목록(Flexbox, `overflow-x: auto` 가로 스크롤)을 배치하십시오. 썸네일은 **5개**이며 각각 **80×80px** `<div>`로 구현하고 서로 다른 배경색을 지정하십시오.
|
||||
* **우측 영역 상단**: 제목, 부제목, 본문 텍스트를 배치하십시오.
|
||||
* **우측 영역 하단**: 탭 3개(개요 / 상세 / 리뷰)와 각 탭의 콘텐츠 영역을 구현하십시오.
|
||||
* **JavaScript 없이** `<input type="radio" name="tab">`과 CSS `:checked` 선택자를 활용하여 선택된 탭의 콘텐츠만 표시되도록 구현하십시오.
|
||||
* `<input type="radio">` 요소는 `display: none`으로 숨기고, `<label>` 요소를 탭 버튼처럼 스타일링하십시오.
|
||||
* 우측 영역 최하단에 Primary 버튼과 Secondary 버튼 2개를 나란히 배치하십시오. Primary 버튼에는 `@keyframes`를 활용하여 호버 시 배경색이 자연스럽게 전환되는 애니메이션을 적용하십시오.
|
||||
|
||||
---
|
||||
|
||||
### ⚡ C. Frontend — JavaScript (Vanilla)
|
||||
|
||||
#### **C1. localStorage 기반 메모 앱** `Easy`
|
||||
|
||||
Vanilla JavaScript와 `localStorage`를 사용하여 간단한 메모 앱을 구현하십시오.
|
||||
|
||||
* 텍스트 입력창(`<textarea>`)과 **"저장"** 버튼이 있어야 합니다.
|
||||
* **"저장"** 버튼 클릭 시 메모가 `localStorage`의 `memos` 키에 JSON 배열로 누적 저장되어야 합니다. 빈 내용은 저장되지 않아야 합니다.
|
||||
* 저장된 메모 목록을 페이지 하단에 렌더링하십시오. 각 항목에는 메모 내용과 **"삭제"** 버튼이 포함되어야 합니다.
|
||||
* **"삭제"** 버튼 클릭 시 해당 항목이 `localStorage`에서 제거되고 목록이 즉시 갱신되어야 합니다.
|
||||
* 페이지 새로고침 후에도 메모 목록이 유지되어야 합니다.
|
||||
|
||||
#### **C2. DOM 조작 및 이벤트 처리** `Easy`
|
||||
|
||||
Vanilla JavaScript를 사용하여 동적 리스트 조작 기능을 구현하십시오.
|
||||
|
||||
* 텍스트 입력창(`<input type="text">`)과 **"추가"** 버튼이 있어야 합니다.
|
||||
* **"추가"** 버튼 클릭 또는 Enter 키 입력 시 입력값이 리스트에 항목으로 추가됩니다. 빈 값은 추가되지 않으며, 추가 후 입력창은 초기화됩니다.
|
||||
* 각 항목에는 **"완료"** 버튼과 **"삭제"** 버튼이 포함됩니다.
|
||||
* **"완료"** 버튼 클릭 시 해당 항목의 텍스트에 취소선(`text-decoration: line-through`)이 적용됩니다. 다시 클릭 시 원래 상태로 돌아옵니다.
|
||||
* **"삭제"** 버튼 클릭 시 해당 항목이 즉시 제거됩니다.
|
||||
* 리스트 상단에 현재 **전체 항목 수**와 **완료된 항목 수**를 실시간으로 표시하십시오.
|
||||
|
||||
#### **C3. JSON 데이터 동적 렌더링 및 필터링** `Normal`
|
||||
|
||||
Vanilla JavaScript와 Fetch API를 사용하여 제공된 로컬 JSON 파일을 받아 동적으로 렌더링하는 페이지를 구현하십시오.
|
||||
|
||||
* 제공된 `posts.json` 파일(`/module_a/c3/posts.json`)을 Fetch API로 불러오십시오. 파일에는 `id`, `title`, `body`, `category` 키를 가진 객체 배열이 포함되어 있습니다. (총 20개)
|
||||
* 불러오는 동안 **로딩 스피너**(CSS 애니메이션)를 표시하고, 완료 후 제거하십시오.
|
||||
* 데이터 전체를 카드 형태로 렌더링하십시오. 각 카드에는 `id`, `title`, `body`, `category`가 표시되어야 합니다.
|
||||
* 제목(`title`)을 기준으로 실시간 필터링이 가능한 검색창을 구현하십시오. (대소문자 구분 없음, 입력할 때마다 즉시 반영)
|
||||
* Fetch 요청 실패 시 **"Failed to load data."** 오류 메시지를 화면에 표시하십시오.
|
||||
|
||||
> **제공 파일**: `posts.json` (선수 작업 디렉토리 `/module_a/c3/`에 미리 배치됨)
|
||||
|
||||
#### **C4. 폼 유효성 검사** `Normal`
|
||||
|
||||
Vanilla JavaScript를 사용하여 실시간 폼 유효성 검사를 구현하십시오.
|
||||
|
||||
* 다음 필드를 포함한 회원 가입 폼을 작성하십시오: **이름**, **이메일**, **비밀번호**, **비밀번호 확인**
|
||||
* 각 필드의 유효성 규칙은 다음과 같습니다.
|
||||
* **이름**: 필수, 2자 이상
|
||||
* **이메일**: 필수, 이메일 형식 (`@`와 `.` 포함 여부로 검사)
|
||||
* **비밀번호**: 필수, 8자 이상, 영문자와 숫자를 모두 포함
|
||||
* **비밀번호 확인**: 필수, 비밀번호 필드와 동일한 값
|
||||
* 각 필드에서 포커스가 벗어날 때(`blur` 이벤트) 해당 필드 아래에 오류 메시지를 표시하십시오. 조건을 충족하면 오류 메시지를 즉시 제거하십시오.
|
||||
* **"제출"** 버튼은 모든 필드가 유효할 때만 활성화됩니다. 클릭 시 `alert("Registration complete.")`를 표시하십시오.
|
||||
|
||||
#### **C5. IntersectionObserver와 지연 렌더링** `Hard`
|
||||
|
||||
`IntersectionObserver`를 활용하여 무한 스크롤과 지연 렌더링을 구현하십시오.
|
||||
|
||||
* 더미 데이터 50개를 JavaScript 배열로 미리 정의하십시오. 각 항목은 `id`, `title`, `color`를 포함하십시오. (`color`는 `#RRGGBB` 형식의 임의 색상값)
|
||||
* 초기 로드 시 아이템을 **10개** 렌더링하십시오. 각 아이템은 높이 **200px**의 색상 `<div>`와 `title` 텍스트로 구성합니다. 초기 10개는 `color` 값을 배경색으로 바로 표시합니다.
|
||||
* 목록 맨 아래에 **sentinel 요소**(`<div id="sentinel">`)를 두고, 해당 요소가 뷰포트에 진입하면 자동으로 다음 10개를 추가 렌더링하십시오.
|
||||
* 아이템이 추가되는 동안 sentinel 바로 앞에 **로딩 스피너 요소**를 삽입하여 화면에 표시하고, 렌더링 완료 후 제거하십시오. (비동기 시뮬레이션: `setTimeout` 600ms 사용)
|
||||
* 총 **50개**에 도달하면 sentinel 감지를 중단(`observer.unobserve`)하고 **"All items loaded."** 메시지를 표시하십시오.
|
||||
* 새로 추가되는 아이템의 색상 `<div>`는 배경색을 `#cccccc`(회색)로 설정한 채로 DOM에 추가하십시오. **별도의 `IntersectionObserver` 인스턴스**를 사용하여 해당 `<div>`가 뷰포트에 진입하는 순간 `data-color` 속성에 저장된 실제 색상값을 배경색으로 교체하십시오.
|
||||
|
||||
---
|
||||
|
||||
### 🛢️ D. Backend — PHP / MySQL
|
||||
|
||||
> **제공 파일**: D4의 `posts_dump.sql`과 D5의 `data_dump.sql`은 해당 Task의 작업 디렉토리에 미리 배치되어 있습니다.
|
||||
|
||||
#### **D1. PDO 기반 CRUD API** `Easy`
|
||||
|
||||
PHP와 MySQL(PDO)을 사용하여 데이터를 처리하는 REST API를 작성하십시오.
|
||||
|
||||
* 아래 테이블 구조로 `items` 테이블을 생성하십시오. 테이블 생성 SQL을 `table.sql`로 함께 제출하십시오.
|
||||
* `id` (INT, PK, AUTO_INCREMENT), `title` (VARCHAR 100, NOT NULL), `content` (TEXT), `created_at` (TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
|
||||
* 다음 4개의 엔드포인트를 단일 파일 `api.php`에 구현하십시오. HTTP 메서드(`$_SERVER['REQUEST_METHOD']`)로 분기하십시오.
|
||||
* `GET /module_a/d1/api.php` → 전체 목록을 JSON 배열로 반환
|
||||
* `POST /module_a/d1/api.php` → 항목 추가. Body(JSON): `{"title": "...", "content": "..."}`. 요청 본문은 `php://input`으로 읽어 `json_decode()`로 파싱하십시오. 성공 시 삽입된 행을 JSON으로 반환
|
||||
* `PUT /module_a/d1/api.php?id={id}` → `title`, `content` 수정. Body(JSON): `{"title": "...", "content": "..."}`. 요청 본문은 `php://input`으로 읽어 `json_decode()`로 파싱하십시오. 성공 시 수정된 행을 JSON으로 반환
|
||||
* `DELETE /module_a/d1/api.php?id={id}` → 항목 삭제. 성공 시 `{"message": "deleted"}` 반환
|
||||
* 존재하지 않는 `id`에 대한 PUT/DELETE 요청 시 HTTP 상태코드 **404**와 `{"error": "Not found"}` 를 반환하십시오.
|
||||
* 모든 응답은 `Content-Type: application/json` 헤더와 함께 반환하십시오.
|
||||
|
||||
#### **D2. 파일 업로드 처리** `Easy`
|
||||
|
||||
PHP를 사용하여 이미지 파일 업로드 기능을 구현하십시오.
|
||||
|
||||
* `upload.php`에 파일 업로드 HTML 폼(GET)과 업로드 처리(POST)를 구현하십시오. 폼의 `enctype`은 `multipart/form-data`로 설정하십시오.
|
||||
* 업로드 허용 조건: 확장자는 **jpg, jpeg, png, gif**만 허용, 파일 크기는 **2MB** 이하.
|
||||
* 조건을 통과한 파일은 `uploads/` 디렉토리에 **`{time()}_{원본파일명}`** 형식으로 저장하십시오. `uploads/` 디렉토리가 없으면 직접 생성하십시오.
|
||||
* 저장 성공 시 업로드된 이미지를 `<img>` 태그로 페이지에 표시하십시오.
|
||||
* 조건 위반 시 오류 메시지를 표시하십시오.
|
||||
* 확장자 불일치: **"File type not allowed."**
|
||||
* 용량 초과: **"File size exceeds 2MB."**
|
||||
|
||||
#### **D3. 세션 기반 인증 시스템** `Normal`
|
||||
|
||||
PHP 세션과 MySQL을 사용하여 회원 가입, 로그인, 로그아웃 기능을 구현하십시오.
|
||||
|
||||
* `users` 테이블: `id` (INT, PK, AUTO_INCREMENT), `email` (VARCHAR 100, UNIQUE), `password` (VARCHAR 255), `name` (VARCHAR 50), `created_at` (TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
|
||||
* **`register.php`**: GET 요청 시 회원 가입 HTML 폼을 출력합니다. POST 요청 시 이메일 중복을 확인하고, `password_hash()`로 비밀번호를 해시하여 DB에 저장한 후 별도 메시지 없이 즉시 `login.php`로 리다이렉트합니다. 이메일 중복 시 폼 위에 **"This email is already registered."** 오류 메시지를 표시합니다.
|
||||
* **`login.php`**: GET 요청 시 로그인 HTML 폼을 출력합니다. POST 요청 시 `password_verify()`로 검증하고, 성공 시 세션에 `user_id`, `name`, `email`을 저장한 후 `mypage.php`로 리다이렉트합니다. 실패 시 폼 위에 **"Incorrect email or password."** 오류 메시지를 표시합니다.
|
||||
* **`logout.php`**: GET 요청 시 `session_destroy()`로 세션을 파기하고 `login.php`로 리다이렉트합니다.
|
||||
* **`mypage.php`**: 세션에 `user_id`가 없으면 `login.php`로 리다이렉트합니다. 로그인 상태에서는 **"Welcome, {name}!"** 메시지와 로그아웃 링크를 화면에 출력합니다.
|
||||
|
||||
#### **D4. 페이지네이션 구현** `Normal`
|
||||
|
||||
PHP와 MySQL(PDO)을 사용하여 서버 사이드 페이지네이션을 구현하십시오.
|
||||
|
||||
* `posts` 테이블을 직접 생성한 뒤, 제공된 SQL 덤프(`posts_dump.sql`)를 임포트하십시오. `posts` 테이블 구조: `id`, `title`, `content`, `created_at`
|
||||
* `index.php`에서 게시글 목록을 **한 페이지에 10개**씩 표시하십시오.
|
||||
* 현재 페이지는 URL 쿼리스트링 `?page={n}`으로 결정되며, 기본값은 1입니다. `LIMIT`과 `OFFSET`을 사용한 SQL 쿼리로 해당 페이지의 데이터만 조회하십시오. `page` 값이 1 미만이거나 전체 페이지 수를 초과하면 1페이지로 리다이렉트하십시오.
|
||||
* 페이지 하단에 페이지네이션 링크를 표시하십시오. **전체 페이지 번호를 모두 표시**하며, 현재 페이지 번호는 활성 스타일로 구분하고, 이전/다음 버튼을 포함하십시오. 첫 페이지에서 이전 버튼, 마지막 페이지에서 다음 버튼은 비활성화하십시오.
|
||||
* 각 게시글 항목에는 제목과 등록일이 표시되며, 제목 클릭 시 `view.php?id={id}`로 이동하여 해당 게시글의 전체 내용을 표시하십시오.
|
||||
|
||||
#### **D5. 다중 테이블 집계 쿼리** `Hard`
|
||||
|
||||
복수 테이블 JOIN과 집계 쿼리를 활용한 통계 API를 구현하십시오.
|
||||
|
||||
* 아래 구조에 맞춰 테이블 3개를 직접 생성한 뒤, 제공된 SQL 덤프(`data_dump.sql`)를 임포트하십시오. **테이블 생성 순서는 `users` → `posts` → `comments` 순서를 지켜야 합니다.** (외래 키 참조 순서)
|
||||
* `users` (`id`, `name`, `created_at`)
|
||||
* `posts` (`id`, `user_id` — users.id 참조, `title`, `category`, `view_count`, `created_at`)
|
||||
* `comments` (`id`, `post_id` — posts.id 참조, `created_at`)
|
||||
* 단일 파일 `stats.php`에서 `type` 쿼리스트링 값으로 분기하여 다음 3개의 응답을 반환하십시오.
|
||||
* `GET /module_a/d5/stats.php?type=daily` → 최근 7일간 **날짜별** 신규 게시글 수를 날짜 오름차순으로 반환. 응답 형식: `[{"date": "2026-04-06", "count": 5}, ...]`
|
||||
* `GET /module_a/d5/stats.php?type=category` → **카테고리별** 총 게시글 수와 평균 조회수를 게시글 수 내림차순으로 반환. 응답 형식: `[{"category": "tech", "post_count": 12, "avg_views": 340}, ...]`
|
||||
* `GET /module_a/d5/stats.php?type=top` → 댓글 수 기준 **상위 5개 게시글**을 댓글 수 내림차순으로 반환. 응답 형식: `[{"post_id": 3, "title": "...", "author": "Alice Johnson", "comment_count": 18}, ...]`
|
||||
* 모든 쿼리는 PDO **Prepared Statement**로 작성하십시오.
|
||||
* 알 수 없는 `type` 값에 대해 HTTP **400**과 `{"error": "Invalid type"}` 을 반환하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 4. 제출 안내 (Submission Guidelines)
|
||||
|
||||
선수는 작업 결과물을 서버 디렉토리 `/module_a/`에 업로드하십시오. 각 Task 결과물은 Task ID를 이름으로 한 하위 디렉토리에 저장해야 합니다. (예: `/module_a/a1/`, `/module_a/b1/`, `/module_a/c1/`, `/module_a/d1/`)
|
||||
|
||||
---
|
||||
|
||||
## 5. 채점 기준 요약 (Marking Scheme — Total 25점)
|
||||
|
||||
| 분류 | Task | 배점 |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Design** | A1. 레이어 합성 및 텍스트 배치 | 0.5 |
|
||||
| | A2. 선택 영역 및 레이어 합성 | 0.5 |
|
||||
| | A3. 색상 보정 및 레이어 마스크 | 1.5 |
|
||||
| | A4. 텍스트 이펙트 및 발광 효과 | 1.5 |
|
||||
| | A5. 텍스트 워프 및 필터 효과 | 2.0 |
|
||||
| **B. Layout** | B1. Flexbox 카드 레이아웃 | 0.5 |
|
||||
| | B2. Sticky 헤더 및 CSS 인터랙션 | 0.5 |
|
||||
| | B3. CSS Grid 2단 레이아웃 | 1.5 |
|
||||
| | B4. CSS 애니메이션 및 트랜지션 | 1.5 |
|
||||
| | B5. CSS Grid 및 고급 선택자 활용 | 2.0 |
|
||||
| **C. Frontend** | C1. localStorage 기반 메모 앱 | 0.5 |
|
||||
| | C2. DOM 조작 및 이벤트 처리 | 0.5 |
|
||||
| | C3. JSON 데이터 동적 렌더링 및 필터링 | 1.5 |
|
||||
| | C4. 폼 유효성 검사 | 1.5 |
|
||||
| | C5. IntersectionObserver와 지연 렌더링 | 2.0 |
|
||||
| **D. Backend** | D1. PDO 기반 CRUD API | 0.5 |
|
||||
| | D2. 파일 업로드 처리 | 0.5 |
|
||||
| | D3. 세션 기반 인증 시스템 | 1.5 |
|
||||
| | D4. 페이지네이션 구현 | 1.5 |
|
||||
| | D5. 다중 테이블 집계 쿼리 | 3.0 |
|
||||
| **합계** | | **25** |
|
||||
Reference in New Issue
Block a user