commit for day 1
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
*.xls*
|
||||
media
|
||||
|
||||
module_c/*
|
||||
!module_c/.gitkeep
|
||||
|
||||
module_d/*
|
||||
!module_d/.gitkeep
|
||||
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 🚀 mini-test-project 2 Release
|
||||
|
||||
The media files will be released right before the competition starts.
|
||||
The details for Modules C and D will be published on the afternoon of April 13th.
|
||||
|
||||
* 📦 **[Module A](./module-a)** ─ `⏱️ 3h`
|
||||
* 📦 **[Module B](./module-b)** ─ `⏱️ 3h`
|
||||
* 📦 **[Module C](./module-c)** ─ `⏱️ 3h`
|
||||
* 📦 **[Module D](./module-d)** ─ `⏱️ 3h`
|
||||
293
module-a/module-a-en.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# ⚡ Test Project: Module A — Speed Test
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
This assessment is a **Speed Test** designed to quickly and accurately evaluate practical proficiency across web technologies. Competitors must complete a total of 20 independent tasks across four categories — **A (Design)**, **B (Layout)**, **C (Frontend)**, and **D (Backend)** — within the allotted time. Each task is independent and may be attempted in any order.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tech Stack & Constraints
|
||||
|
||||
| Category | Allowed Technologies | Restrictions |
|
||||
| :--- | :--- | :--- |
|
||||
| **A. Design** | GIMP | No other image editors (e.g., Photoshop) permitted |
|
||||
| **B. Layout** | HTML, CSS | JavaScript is not permitted |
|
||||
| **C. Frontend** | JavaScript (Vanilla) | No external libraries or frameworks permitted |
|
||||
| **D. Backend** | PHP, MySQL | Database must be connected using PDO |
|
||||
|
||||
* All deliverables will be evaluated in a desktop Chrome browser.
|
||||
* Save each task's deliverable in a subdirectory named after the task ID under `/module_a/`. (e.g., `/module_a/a1/`, `/module_a/b1/`)
|
||||
* Design tasks must include the GIMP source file (`.xcf`) in the submission directory.
|
||||
|
||||
---
|
||||
|
||||
## 3. Tasks
|
||||
|
||||
### 🎨 A. Design — GIMP Proficiency
|
||||
|
||||
> **Provided files**: Files for A1 (`source.png`), A2 (`base.jpg`), and A3 (`photo.jpg`, `mask.png`) are pre-placed in each task's working directory. A4 and A5 have no provided files; competitors must create all assets from scratch.
|
||||
|
||||
#### **A1. Layer Compositing and Text Placement** `Easy`
|
||||
|
||||
Create an image in GIMP using a layered structure.
|
||||
|
||||
* Canvas size: **800×400px**, Resolution: **72dpi**
|
||||
* **Layer 1**: Fill the background with the solid color `#2C3E50`.
|
||||
* **Layer 2**: Open the provided image (`source.png`), place it at the **center** of the canvas, and set its opacity to **70%**.
|
||||
* **Layer 3**: Add the text **"Hello, GIMP"** in white (`#FFFFFF`) at 36pt, and position it at the top-left of the canvas **(x: 20px, y: 20px)**.
|
||||
* Save the project as a `.xcf` file first. Then flatten all layers (Flatten Image) and export the result as `result.png`.
|
||||
|
||||
#### **A2. Selection and Layer Compositing** `Easy`
|
||||
|
||||
Apply shape effects to an image using GIMP's selection tools and layers.
|
||||
|
||||
* Open the provided image (`base.jpg`) as the background layer.
|
||||
* **Effect 1**: Add a new layer. Use the Rectangle Select tool to select the region from **(0, 0) to (200, 200)** in the top-left corner, fill it with `#E74C3C`, and set this layer's opacity to **60%**.
|
||||
* **Effect 2**: Add another new layer. Create an elliptical selection with a diameter of **150px** at the **center** of the image. Go to `Selection → Border`, set the border width to **3px**, and fill the resulting selection with `#FFFFFF`.
|
||||
* Save the project as a `.xcf` file first. Then flatten all layers (Flatten Image) and export the result as `result.jpg`.
|
||||
|
||||
#### **A3. Color Correction and Layer Mask** `Normal`
|
||||
|
||||
Apply color correction and a layer mask to the provided image (`photo.jpg`) in GIMP.
|
||||
|
||||
* **Color correction**: Use the Hue-Saturation tool to increase Saturation by **+40**.
|
||||
* **Curves adjustment**: Use the Curves tool to raise the output value of highlights by **+20 or more** and lower the output value of shadows by **-20 or less** to increase contrast.
|
||||
* **Layer mask**: Add a layer mask to the image layer. Paste the provided mask image (`mask.png`) into the mask layer so that the background outside the subject is removed.
|
||||
* Save the project as a `.xcf` file first. Then export the result as `result.png`, ensuring the transparent background is preserved (PNG format).
|
||||
|
||||
#### **A4. Text Effect and Glow** `Normal`
|
||||
|
||||
Apply a glow visual effect to text in GIMP.
|
||||
|
||||
* Canvas size: **600×200px**, Background color: `#1A1A2E`
|
||||
* Add the text **"SPEED TEST"** in white (`#FFFFFF`), Bold, 60pt, centered on the canvas.
|
||||
* Duplicate the text layer and place the duplicate **below** the original text layer.
|
||||
* Rasterize the duplicate layer (Layer → Rasterize). In the Layers panel, set the duplicate layer's **Mode** to **`Color`** first. Then set the foreground color to `#4A90D9` and apply `Edit → Fill with Foreground Color` so the blue color is applied.
|
||||
* Apply **Gaussian Blur** with a radius of **8px** to the duplicate layer to complete the outer glow effect.
|
||||
* Save the project as a `.xcf` file first, then export the result as `result.png`.
|
||||
|
||||
#### **A5. Text Warp and Filter Effects** `Hard`
|
||||
|
||||
Rasterize text and apply a combination of Distorts filters in GIMP to create visual distortion effects.
|
||||
|
||||
* Canvas size: **800×300px**, Background color: `#0D0D0D`
|
||||
* Add the text **"DISTORTION"** in white (`#FFFFFF`), Bold, 72pt, centered on the canvas.
|
||||
* Rasterize the text layer (Layer → Rasterize).
|
||||
* Apply the following two Distorts filters to the rasterized text layer in order:
|
||||
* **Ripple**: Amplitude **8**, Wavelength **40**, Orientation: Horizontal
|
||||
* **Whirl and Pinch**: Whirl angle **20** degrees, Pinch **0**, Radius **1.0**
|
||||
* Duplicate the distorted text layer. Apply **Gaussian Blur** with a radius of **3px** to the duplicate, then place the duplicate **below** the original layer to create a ghosting effect.
|
||||
* Save the project as a `.xcf` file first, then export the result as `result.png`.
|
||||
|
||||
---
|
||||
|
||||
### 📐 B. Layout — HTML / CSS Only
|
||||
|
||||
#### **B1. Flexbox Card Layout** `Easy`
|
||||
|
||||
Build a card list layout using HTML and CSS only.
|
||||
|
||||
* Use **Flexbox** to arrange cards **4 per row**. (Use `flex-wrap: wrap`)
|
||||
* Each card must contain: an image area (`<div>`, height **160px**, with a background color), a title (`<h3>`), body text (`<p>`), and a button (`<button>`).
|
||||
* Set the gap between cards to **24px**.
|
||||
* Create **8** dummy cards. Each card's image area must have a different background color.
|
||||
|
||||
#### **B2. Sticky Header and CSS Interaction** `Easy`
|
||||
|
||||
Build a header with CSS-only interactions using HTML and CSS only.
|
||||
|
||||
* Use **Flexbox** to arrange the header with a logo text on the left, 4 menu links in the center, and 2 buttons on the right.
|
||||
* Implement a CSS-only interaction where hovering over a menu link reveals a **2px solid underline** with a `transition` effect. (Use `border-bottom` or a `::after` pseudo-element)
|
||||
* Use `position: sticky; top: 0;` to keep the header fixed at the top while scrolling.
|
||||
* Place dummy content with a minimum height of **3000px** below the header to enable scrolling.
|
||||
|
||||
#### **B3. CSS Grid Two-Column Layout** `Normal`
|
||||
|
||||
Build a two-column page layout using CSS Grid with HTML and CSS only.
|
||||
|
||||
* Use **CSS Grid** to create a layout with a fixed left sidebar (**250px**) and a right main content area (`1fr`).
|
||||
* Place a vertical navigation menu (5 items) in the left sidebar. Style the first item to indicate an active state using background color and text color.
|
||||
* Place a title, body text, and a **3-column card grid** (using CSS Grid, 6 cards) in sequence in the right main area.
|
||||
* Apply `height: 100vh` and `overflow: hidden` to the layout wrapper. Apply `height: 100vh` and `overflow-y: auto` to both the sidebar and the main area so that each scrolls independently.
|
||||
|
||||
#### **B4. CSS Animations and Transitions** `Normal`
|
||||
|
||||
Implement various CSS animations and transition effects on a single page using HTML and CSS only.
|
||||
|
||||
* **Card hover effect**: Arrange 4 cards in a row using Flexbox. On hover, each card should move up **8px** and have a stronger shadow using a transition. (Use `transform: translateY(-8px)`, `box-shadow`, and `transition`)
|
||||
* **Loading spinner**: Use `@keyframes` to create a circular loading spinner. The spinner should be **48px** in diameter with a **4px** border. Only the top border (`border-top`) should be colored `#3498DB`; the remaining borders should be `#e0e0e0`. The spinner rotates infinitely.
|
||||
* **Fade-in text**: On page load, a heading (`<h1>`) should animate from `translateY(20px)` to `translateY(0)` while fading from `opacity: 0` to `opacity: 1` using `@keyframes`. (`animation-duration: 0.8s`, `animation-fill-mode: both`)
|
||||
|
||||
#### **B5. CSS Grid and Advanced Selectors** `Hard`
|
||||
|
||||
Build a two-column content layout with a CSS-only tab interaction using HTML and CSS only.
|
||||
|
||||
* Use **CSS Grid** to create a two-column layout: left column (65%) and right column (35%).
|
||||
* **Left area**: Place a main image area (`<div>`, height **300px**, with a background color) and a horizontal thumbnail list below it (Flexbox, `overflow-x: auto`). Include **5** thumbnails, each as an **80×80px** `<div>` with a different background color.
|
||||
* **Right area — upper**: Place a title, subtitle, and body text.
|
||||
* **Right area — lower**: Implement 3 tabs (Overview / Details / Reviews) with their respective content areas.
|
||||
* Use `<input type="radio" name="tab">` and the CSS `:checked` selector **without JavaScript** so that only the selected tab's content is shown.
|
||||
* Hide the `<input type="radio">` elements with `display: none` and style the corresponding `<label>` elements to look like tab buttons.
|
||||
* At the bottom of the right area, place a Primary button and a Secondary button side by side. Apply a `@keyframes` animation to the Primary button so its background color transitions smoothly on hover.
|
||||
|
||||
---
|
||||
|
||||
### ⚡ C. Frontend — JavaScript (Vanilla)
|
||||
|
||||
#### **C1. localStorage-based Memo App** `Easy`
|
||||
|
||||
Build a simple memo app using Vanilla JavaScript and `localStorage`.
|
||||
|
||||
* The page must include a text input (`<textarea>`) and a **"Save"** button.
|
||||
* Clicking **"Save"** adds the memo to a JSON array stored under the `memos` key in `localStorage`. Empty content must not be saved.
|
||||
* Render the saved memo list below the input area. Each item must include the memo content and a **"Delete"** button.
|
||||
* Clicking **"Delete"** removes the item from `localStorage` and immediately updates the rendered list.
|
||||
* The memo list must persist after a page refresh.
|
||||
|
||||
#### **C2. DOM Manipulation and Event Handling** `Easy`
|
||||
|
||||
Build a dynamic list with item management using Vanilla JavaScript.
|
||||
|
||||
* The page must include a text input (`<input type="text">`) and an **"Add"** button.
|
||||
* Clicking **"Add"** or pressing Enter adds the input value as a new list item. Empty values must not be added. The input field is cleared after adding.
|
||||
* Each item must include a **"Done"** button and a **"Delete"** button.
|
||||
* Clicking **"Done"** applies a strikethrough (`text-decoration: line-through`) to the item text. Clicking again toggles it back.
|
||||
* Clicking **"Delete"** removes the item immediately.
|
||||
* Display the current **total item count** and **completed item count** at the top of the list in real time.
|
||||
|
||||
#### **C3. Dynamic JSON Rendering and Filtering** `Normal`
|
||||
|
||||
Build a page that fetches a local JSON file and dynamically renders its content using Vanilla JavaScript and the Fetch API.
|
||||
|
||||
* Fetch the provided `posts.json` file at `/module_a/c3/posts.json` using the Fetch API. The file contains an array of objects with the keys `id`, `title`, `body`, and `category`. (20 items total)
|
||||
* Display a **loading spinner** (CSS animation) while fetching, and remove it when the fetch completes.
|
||||
* Render all items as cards. Each card must display `id`, `title`, `body`, and `category`.
|
||||
* Implement a search input that filters cards in real time by `title`. (Case-insensitive; updates immediately on each keystroke)
|
||||
* If the Fetch request fails, display the error message **"Failed to load data."** on the screen.
|
||||
|
||||
> **Provided file**: `posts.json` (pre-placed in the competitor's working directory at `/module_a/c3/`)
|
||||
|
||||
#### **C4. Form Validation** `Normal`
|
||||
|
||||
Implement real-time form validation using Vanilla JavaScript.
|
||||
|
||||
* Build a registration form with the following fields: **Name**, **Email**, **Password**, **Confirm Password**
|
||||
* Validation rules for each field:
|
||||
* **Name**: Required, at least 2 characters
|
||||
* **Email**: Required, must contain both `@` and `.`
|
||||
* **Password**: Required, at least 8 characters, must contain both letters and numbers
|
||||
* **Confirm Password**: Required, must match the Password field exactly
|
||||
* When focus leaves a field (`blur` event), display an error message below that field if the validation rule is not met. Remove the error message immediately once the condition is satisfied.
|
||||
* The **"Submit"** button must only be enabled when all fields are valid. Clicking it should trigger `alert("Registration complete.")`.
|
||||
|
||||
#### **C5. IntersectionObserver and Deferred Rendering** `Hard`
|
||||
|
||||
Implement infinite scroll and deferred rendering using `IntersectionObserver`.
|
||||
|
||||
* Define an array of 50 dummy items in JavaScript. Each item must have `id`, `title`, and `color`. (`color` is an arbitrary hex color value in `#RRGGBB` format)
|
||||
* On initial load, render **10** items. Each item consists of a **200px-tall** color `<div>` and its `title` text. The first 10 items should display their `color` value as the background color immediately.
|
||||
* Place a **sentinel element** (`<div id="sentinel">`) at the bottom of the list. When the sentinel enters the viewport, automatically render the next 10 items.
|
||||
* While new items are being added, insert a **loading spinner element** directly before the sentinel to display it, and remove it once rendering is complete. (Simulate async behavior using `setTimeout` with 600ms)
|
||||
* Once all **50 items** have been rendered, stop observing the sentinel (`observer.unobserve`) and display the message **"All items loaded."**
|
||||
* New items should be added to the DOM with their color `<div>` initially set to `#cccccc` (grey). Use a **separate `IntersectionObserver` instance** to watch each `<div>`, and replace the background color with the real color from the element's `data-color` attribute the moment it enters the viewport.
|
||||
|
||||
---
|
||||
|
||||
### 🛢️ D. Backend — PHP / MySQL
|
||||
|
||||
> **Provided files**: `posts_dump.sql` for D4 and `data_dump.sql` for D5 are pre-placed in each task's working directory.
|
||||
|
||||
#### **D1. PDO-based CRUD API** `Easy`
|
||||
|
||||
Build a REST API to handle data using PHP and MySQL (PDO).
|
||||
|
||||
* Create an `items` table using the structure below. Submit the table creation SQL as `table.sql`.
|
||||
* `id` (INT, PK, AUTO_INCREMENT), `title` (VARCHAR 100, NOT NULL), `content` (TEXT), `created_at` (TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
|
||||
* Implement the following 4 endpoints in a single file `api.php`, branching by HTTP method (`$_SERVER['REQUEST_METHOD']`).
|
||||
* `GET /module_a/d1/api.php` → Return all items as a JSON array
|
||||
* `POST /module_a/d1/api.php` → Add a new item. Body (JSON): `{"title": "...", "content": "..."}`. Read the request body using `php://input` and parse it with `json_decode()`. On success, return the inserted row as JSON.
|
||||
* `PUT /module_a/d1/api.php?id={id}` → Update `title` and `content`. Body (JSON): `{"title": "...", "content": "..."}`. Read the request body using `php://input` and parse it with `json_decode()`. On success, return the updated row as JSON.
|
||||
* `DELETE /module_a/d1/api.php?id={id}` → Delete the item. On success, return `{"message": "deleted"}`.
|
||||
* For PUT/DELETE requests with a non-existent `id`, return HTTP status **404** and `{"error": "Not found"}`.
|
||||
* All responses must include a `Content-Type: application/json` header.
|
||||
|
||||
#### **D2. File Upload Handling** `Easy`
|
||||
|
||||
Implement image file upload functionality using PHP.
|
||||
|
||||
* In `upload.php`, implement both a file upload HTML form (GET) and the upload processing logic (POST). Set the form's `enctype` to `multipart/form-data`.
|
||||
* Upload conditions: only **jpg, jpeg, png, gif** extensions are allowed; file size must be **2MB or less**.
|
||||
* Save files that pass validation to the `uploads/` directory using the naming format **`{time()}_{original_filename}`**. If the `uploads/` directory does not exist, create it manually.
|
||||
* On successful upload, display the uploaded image using an `<img>` tag.
|
||||
* For validation failures, display the appropriate error message:
|
||||
* Invalid extension: **"File type not allowed."**
|
||||
* Size exceeded: **"File size exceeds 2MB."**
|
||||
|
||||
#### **D3. Session-based Authentication System** `Normal`
|
||||
|
||||
Implement user registration, login, and logout using PHP sessions and MySQL.
|
||||
|
||||
* `users` table: `id` (INT, PK, AUTO_INCREMENT), `email` (VARCHAR 100, UNIQUE), `password` (VARCHAR 255), `name` (VARCHAR 50), `created_at` (TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
|
||||
* **`register.php`**: On GET, render an HTML registration form. On POST, check for duplicate emails and save the password hashed with `password_hash()`, then redirect immediately to `login.php` without displaying a success message. If the email is already in use, display the error **"This email is already registered."** above the form.
|
||||
* **`login.php`**: On GET, render an HTML login form. On POST, verify the password with `password_verify()`. On success, store `user_id`, `name`, and `email` in the session and redirect to `mypage.php`. On failure, display the error **"Incorrect email or password."** above the form.
|
||||
* **`logout.php`**: On GET, destroy the session with `session_destroy()` and redirect to `login.php`.
|
||||
* **`mypage.php`**: If `user_id` is not in the session, redirect to `login.php`. If logged in, display the message **"Welcome, {name}!"** and a logout link.
|
||||
|
||||
#### **D4. Pagination** `Normal`
|
||||
|
||||
Implement server-side pagination using PHP and MySQL (PDO).
|
||||
|
||||
* Create the `posts` table yourself, then import the provided SQL dump (`posts_dump.sql`). Table structure: `id`, `title`, `content`, `created_at`
|
||||
* In `index.php`, display **10 posts per page**.
|
||||
* The current page is determined by the URL query string `?page={n}`, defaulting to 1. Use `LIMIT` and `OFFSET` in your SQL query to retrieve only the data for the current page. If `page` is less than 1 or exceeds the total number of pages, redirect to page 1.
|
||||
* Display pagination links at the bottom of the page. **Show all page numbers**, highlight the current page with an active style, and include Previous/Next buttons. Disable the Previous button on the first page and the Next button on the last page.
|
||||
* Each post item must show the title and registration date. Clicking the title navigates to `view.php?id={id}`, which displays the full content of that post.
|
||||
|
||||
#### **D5. Multi-table Aggregate Queries** `Hard`
|
||||
|
||||
Implement a statistics API using multi-table JOINs and aggregate queries.
|
||||
|
||||
* Create all 3 tables yourself, then import the provided SQL dump (`data_dump.sql`). **Tables must be created in the order `users` → `posts` → `comments`** (to satisfy foreign key dependencies).
|
||||
* `users` (`id`, `name`, `created_at`)
|
||||
* `posts` (`id`, `user_id` — references users.id, `title`, `category`, `view_count`, `created_at`)
|
||||
* `comments` (`id`, `post_id` — references posts.id, `created_at`)
|
||||
* In a single file `stats.php`, branch on the `type` query string and return the following 3 responses:
|
||||
* `GET /module_a/d5/stats.php?type=daily` → Return the number of new posts per day for the past 7 days, sorted by date ascending. Response format: `[{"date": "2026-04-06", "count": 5}, ...]`
|
||||
* `GET /module_a/d5/stats.php?type=category` → Return the total post count and average view count per category, sorted by post count descending. Response format: `[{"category": "tech", "post_count": 12, "avg_views": 340}, ...]`
|
||||
* `GET /module_a/d5/stats.php?type=top` → Return the top 5 posts by comment count, sorted by comment count descending. Response format: `[{"post_id": 3, "title": "...", "author": "Alice Johnson", "comment_count": 18}, ...]`
|
||||
* All queries must use PDO **Prepared Statements**.
|
||||
* For unknown `type` values, return HTTP **400** and `{"error": "Invalid type"}`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Submission Guidelines
|
||||
|
||||
Upload all deliverables to the server directory `/module_a/`. Each task's output must be saved in a subdirectory named after the task ID. (e.g., `/module_a/a1/`, `/module_a/b1/`, `/module_a/c1/`, `/module_a/d1/`)
|
||||
|
||||
---
|
||||
|
||||
## 5. Marking Scheme — Total 25 Points
|
||||
|
||||
| Category | Task | Points |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Design** | A1. Layer Compositing and Text Placement | 0.5 |
|
||||
| | A2. Selection and Layer Compositing | 0.5 |
|
||||
| | A3. Color Correction and Layer Mask | 1.5 |
|
||||
| | A4. Text Effect and Glow | 1.5 |
|
||||
| | A5. Text Warp and Filter Effects | 2.0 |
|
||||
| **B. Layout** | B1. Flexbox Card Layout | 0.5 |
|
||||
| | B2. Sticky Header and CSS Interaction | 0.5 |
|
||||
| | B3. CSS Grid Two-Column Layout | 1.5 |
|
||||
| | B4. CSS Animations and Transitions | 1.5 |
|
||||
| | B5. CSS Grid and Advanced Selectors | 2.0 |
|
||||
| **C. Frontend** | C1. localStorage-based Memo App | 0.5 |
|
||||
| | C2. DOM Manipulation and Event Handling | 0.5 |
|
||||
| | C3. Dynamic JSON Rendering and Filtering | 1.5 |
|
||||
| | C4. Form Validation | 1.5 |
|
||||
| | C5. IntersectionObserver and Deferred Rendering | 2.0 |
|
||||
| **D. Backend** | D1. PDO-based CRUD API | 0.5 |
|
||||
| | D2. File Upload Handling | 0.5 |
|
||||
| | D3. Session-based Authentication System | 1.5 |
|
||||
| | D4. Pagination | 1.5 |
|
||||
| | D5. Multi-table Aggregate Queries | 3.0 |
|
||||
| **Total** | | **25** |
|
||||
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** |
|
||||
203
module-b/module-b-en.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 🛍️ Test Project: Module B — ShopPress (Online Shopping Mall)
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
In the rapidly growing e-commerce market, the startup **ShopPress** is launching its own shopping mall platform that allows administrators to manage products and orders directly. Administrators can register and manage products and process orders, while general visitors can browse and purchase products. In this project, competitors are responsible for everything from database design to core feature implementation. This is an MVP, so focus on implementing the essential functionality. **Design is not a graded criterion.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Tech Stack & Constraints
|
||||
|
||||
* **Tech Stack**: There are no restrictions on programming language or framework. You may freely choose any server-side rendering (SSR)-capable framework such as Laravel, Django, Ruby on Rails, or Spring Boot.
|
||||
* **Desktop Environment**: Responsive design is not required. The application only needs to work in a desktop Chrome browser.
|
||||
* **Image Upload**: Product images must be JPG or PNG format only, with a file size limit of **5MB**. This validation must be performed **server-side**.
|
||||
* **Database**: Categories must be managed in a separate table (`categories`). The products table (`products`) must reference `category_id` as a foreign key (FK).
|
||||
* **Initial Data**: The following data must be created automatically on first run (via Seeder or Migration).
|
||||
* Admin account: Name `Admin`, Email `admin@shoppress.local`, Password `password` (password must be stored as a hash)
|
||||
* At least 3 initial categories (e.g., `Clothing`, `Electronics`, `Food`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Tasks to Complete
|
||||
|
||||
### **1. Authentication**
|
||||
|
||||
#### 1.1 Login and Session Management
|
||||
|
||||
The login page is accessible at `/admin/login`. The user enters their email and password. On successful login, the user is redirected to the admin dashboard. On failure, display the message **"Incorrect email or password."**
|
||||
|
||||
Accessing any route under `/admin` without being logged in redirects the user to the login page. Logging out destroys the session and redirects to the login page.
|
||||
|
||||
The currently logged-in admin's name must be displayed in the navigation on all admin pages.
|
||||
|
||||
---
|
||||
|
||||
### **2. Admin Features**
|
||||
|
||||
#### 2.1 Dashboard
|
||||
|
||||
The dashboard is the default admin page (`/admin` or `/admin/dashboard`) and is the first page shown after login.
|
||||
|
||||
Display the following 4 statistics as cards on the dashboard.
|
||||
|
||||
| Card Label | Content |
|
||||
| :--- | :--- |
|
||||
| **Total Products** | Total number of products |
|
||||
| **Active Products** | Number of products with status `Active` |
|
||||
| **Total Orders** | Total number of orders |
|
||||
| **Total Revenue** | Sum of all final payment amounts (after coupon discounts) |
|
||||
|
||||
Also display a list of the 5 most recently placed orders. Each row must include: Order # , Customer, Total, Status, and Date.
|
||||
|
||||
#### 2.2 Product Management
|
||||
|
||||
The product management page (`/admin/products`) displays all products in a table.
|
||||
|
||||
Table columns: **Name**, **Category**, **Price**, **Stock**, **Status**, **Created At**, Edit / Delete buttons.
|
||||
|
||||
Provide a search field to filter products by name. Submitting a search term displays only products whose name contains that keyword.
|
||||
|
||||
The **product registration form** (`/admin/products/new`) and **product edit form** (`/admin/products/{id}/edit`) must include the following fields.
|
||||
|
||||
* **Name** (required, max 100 characters)
|
||||
* **Category** (required, dropdown — populated from categories in the database)
|
||||
* **Price** (required, integer, 0 or greater)
|
||||
* **Stock** (required, integer, 0 or greater)
|
||||
* **Description** (`<textarea>`, optional)
|
||||
* **Image** (file upload, JPG/PNG, max 5MB, **optional** — products can be registered without an image)
|
||||
* **Status** (dropdown: `Active` / `Out of Stock` / `Hidden`)
|
||||
|
||||
When entering the edit form, existing data must be pre-filled in each field. If no new image is uploaded, the existing image is retained. If a new image is uploaded, delete the existing image file from the server and replace it with the new one.
|
||||
|
||||
When deleting a product, display the confirmation message **"Are you sure you want to delete this product?"** On confirmation, if the product appears in any order (`order_items`), reject the deletion and display **"Cannot delete a product with order history."** If the product has no order history, delete it from the database and remove its image file from the server.
|
||||
|
||||
#### 2.3 Category Management
|
||||
|
||||
The category management page (`/admin/categories`) allows adding, editing, and deleting categories.
|
||||
|
||||
* **Add**: Enter a category name and save. If the name already exists, display **"Category name already exists."**
|
||||
* **Edit**: Change the category name. Since products reference the category via FK, updating the category name in the `categories` table is automatically reflected in associated products.
|
||||
* **Delete**: If any product references this category, reject the deletion and display **"Cannot delete a category that has products assigned."**
|
||||
|
||||
#### 2.4 Coupon Management
|
||||
|
||||
The coupon management page (`/admin/coupons`) allows registering and managing discount coupons.
|
||||
|
||||
The **coupon registration form** includes the following fields.
|
||||
|
||||
* **Coupon Code** (required, uppercase letters or digits only, 6–12 characters, must be unique)
|
||||
* **Discount Type** (dropdown: `Fixed` / `Percent`)
|
||||
* **Discount Value** (required, integer, 1 or greater. If `Percent`, must be between 1 and 100)
|
||||
* **Minimum Order Amount** (required, integer, 0 or greater. Coupon cannot be applied to orders below this amount)
|
||||
* **Expires At** (required, `<input type="date">`. Only future dates may be selected)
|
||||
* **Usage Limit** (required, integer, 1 or greater)
|
||||
|
||||
Coupon list table columns: **Code**, **Type**, **Value**, **Min. Order**, **Expires At**, **Remaining Uses**, Delete button. Remaining uses are managed via the `remaining_uses` column, initialized to the Usage Limit value on registration and decremented by 1 each time the coupon is used.
|
||||
|
||||
#### 2.5 Order Management
|
||||
|
||||
The order management page (`/admin/orders`) displays all orders in a table.
|
||||
|
||||
Table columns: **Order #**, **Customer**, **Phone**, **Total**, **Status**, **Date**.
|
||||
|
||||
Provide a dropdown to filter orders by status. Status values are `Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`. Only orders matching the selected status are shown.
|
||||
|
||||
The order detail page (`/admin/orders/{id}`) must display the following information.
|
||||
|
||||
* Customer Info: Name, Phone, Shipping Address
|
||||
* Order Items: Product (product name snapshot), Unit Price, Qty, Subtotal
|
||||
* Coupon: Applied coupon code and discount amount. Display **"None"** if no coupon was applied.
|
||||
* Total: Final payment amount
|
||||
* **Order Status** dropdown (`Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`) and a Save button. On successful save, display **"Order status updated."** on the same page.
|
||||
* When the status is changed to **`Cancelled`**, restore the stock of all products in the order by their ordered quantities. If the order is already `Cancelled` and the status is set to `Cancelled` again, do not restore stock a second time.
|
||||
|
||||
---
|
||||
|
||||
### **3. Public Pages**
|
||||
|
||||
#### 3.1 Home — Product List
|
||||
|
||||
The home page (`/`) displays products with status **`Active`** as cards. Each card shows the product image (display a grey placeholder area if no image exists), product name, category name, and price.
|
||||
|
||||
Display **12 items per page** with pagination. Sort by most recently registered first.
|
||||
|
||||
Provide category filter links. Clicking a category shows only **`Active`** products in that category. Pagination must retain the current category filter.
|
||||
|
||||
Provide a keyword search field. Products are searched by name. The category filter and keyword search must be applied simultaneously. Pagination must retain the search conditions.
|
||||
|
||||
#### 3.2 Product Detail
|
||||
|
||||
The product detail page is accessible at `/products/{id}`. Return a 404 page for non-existent IDs or products with status **`Hidden`**.
|
||||
|
||||
The product detail page displays the product image (grey placeholder if no image), product name, category name, price, stock quantity, and description.
|
||||
|
||||
Products with stock of 0 or status **`Out of Stock`** show an **"Out of Stock"** message and have the purchase button disabled.
|
||||
|
||||
Products with stock of 1 or more and status **`Active`** show a quantity input (`<input type="number">`, min 1, max current stock) and a **"Buy Now"** button. Clicking the button passes the product ID and quantity as query strings and navigates to the checkout page (`/checkout?product_id={id}&quantity={n}`).
|
||||
|
||||
#### 3.3 Checkout
|
||||
|
||||
The checkout page (`/checkout`) reads `product_id` and `quantity` from the query string and displays the order summary (product name, unit price, quantity, subtotal). The server must validate the following conditions and redirect to the home page (`/`) if any are not met.
|
||||
|
||||
* The product with `product_id` does not exist
|
||||
* The product status is **`Hidden`** or **`Out of Stock`**
|
||||
* Stock is 0
|
||||
* `quantity` is less than 1 or exceeds current stock
|
||||
|
||||
The page includes a form for the buyer's Name, Phone, and Shipping Address. All fields are required and orders are only processed after passing **server-side validation**.
|
||||
|
||||
There is a **Coupon Code** input field and an **"Apply"** button. When "Apply" is clicked, use `fetch` to send a POST request to the `/checkout/coupon` endpoint in the format below, and reflect the result on the page without a full reload. This endpoint must be implemented by the competitor.
|
||||
|
||||
* Request Body (JSON): `{ "coupon_code": "ABCD1234", "subtotal": 30000 }`
|
||||
* Success Response (JSON): `{ "valid": true, "discount_type": "fixed"|"percent", "discount_value": 3000, "discount_amount": 3000, "final_amount": 27000 }`
|
||||
* Failure Response (JSON): `{ "valid": false, "message": "error message" }`
|
||||
|
||||
* Valid coupon: Calculate the discount and display the updated Subtotal, Discount, and Total on the page.
|
||||
* `Fixed` discount: Subtract the discount value directly from the subtotal.
|
||||
* `Percent` discount: Deduct `subtotal × discount rate / 100`. Round down (floor) any decimal.
|
||||
* The final amount must not go below 0.
|
||||
* Invalid coupon: Display the appropriate error message for each case.
|
||||
* Code does not exist: **"Invalid coupon code."**
|
||||
* Coupon has expired: **"This coupon has expired."**
|
||||
* Usage limit reached: **"This coupon has reached its usage limit."**
|
||||
* Below minimum order amount: **"This coupon requires a minimum order of {n}."**
|
||||
|
||||
Process the following as a **single transaction** when submitting the order.
|
||||
1. Save order information to the `orders` table (buyer name, phone, shipping address, applied coupon ID — NULL if none, discount amount — 0 if none, final payment amount).
|
||||
2. Save order item information to the `order_items` table (order ID, product ID, **product name snapshot**, quantity, unit price). The product name must be saved as it was at the time of the order so that it displays correctly even if the product is later deleted or renamed.
|
||||
3. Deduct the ordered quantity from the product's stock (`stock`).
|
||||
4. If a coupon was applied, decrement the coupon's `remaining_uses` by 1.
|
||||
|
||||
If stock is insufficient for the requested quantity, roll back the transaction and display **"Not enough stock."**
|
||||
|
||||
On successful order submission, redirect to the order complete page (`/orders/{id}/complete`). This page is accessible without authentication and displays the Order #, order item details, Discount, and Total.
|
||||
|
||||
#### 3.4 Layout
|
||||
|
||||
All public pages include a header and footer. The header contains the site name (**ShopPress**) and a link to the home page. Each page must have an appropriate `<title>` tag reflecting the page content.
|
||||
|
||||
Admin panel pages include a sidebar or top navigation. Navigation menu items: **Dashboard**, **Products**, **Categories**, **Coupons**, **Orders**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Submission Guidelines
|
||||
|
||||
Upload all deliverables to the server directory `/module_b/`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Marking Scheme — Total 25 Points
|
||||
|
||||
| Item | Criteria | Points |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Authentication** | Login/logout, session protection, unauthenticated access redirect | 2 |
|
||||
| **B. Dashboard** | 4 statistics cards (English labels), recent 5 orders list | 1 |
|
||||
| **C. Product Management** | List (with search), register (optional image), edit (retain/replace+delete old image), delete (reject if order history exists + delete image file) | 5 |
|
||||
| **D. Category Management** | Add (duplicate check), edit, delete (reject if products assigned) | 2 |
|
||||
| **E. Coupon Management** | Register (full validation), list (remaining uses display), delete | 3 |
|
||||
| **F. Order Management** | List (status filter), detail view, status update, stock restore on Cancelled (prevent duplicate restore) | 4 |
|
||||
| **G. Public — Product List** | Active product filtering, category filter, keyword search, pagination (conditions retained) | 3 |
|
||||
| **H. Public — Product Detail** | Product info display, Out of Stock handling (stock 0 or Out of Stock status), quantity input and checkout navigation | 1 |
|
||||
| **I. Public — Checkout** | Entry validation (Out of Stock/Hidden/stock exceeded), coupon application (fetch + discount calculation), transaction (save order + deduct stock + deduct coupon uses), order complete page | 3 |
|
||||
| **J. Validation** | Server-side image size/extension check, checkout form field validation | 1 |
|
||||
| **Total** | | **25** |
|
||||
203
module-b/module-b-kr.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 🛍️ Test Project: Module B — ShopPress (온라인 쇼핑몰)
|
||||
|
||||
## 1. 프로젝트 개요 (Project Overview)
|
||||
|
||||
빠르게 성장하는 이커머스 시장에서 스타트업 **ShopPress**는 관리자가 상품과 주문을 직접 운영할 수 있는 자체 쇼핑몰 플랫폼을 런칭하려 합니다. 관리자는 상품을 등록/관리하고 주문을 처리하며, 일반 방문자는 상품을 탐색하고 구매할 수 있습니다. 본 과제에서 선수는 데이터베이스 설계부터 핵심 기능 구현까지 전담합니다. 이 프로젝트는 MVP이므로 핵심 기능 구현에 집중하십시오. **디자인은 채점 항목이 아닙니다.**
|
||||
|
||||
---
|
||||
|
||||
## 2. 기술 스택 및 제약 조건 (Tech Stack & Constraints)
|
||||
|
||||
* **기술 스택**: 사용 언어 및 프레임워크에 제한이 없습니다. Laravel, Django, Ruby on Rails, Spring Boot 등 서버 사이드 렌더링(SSR)이 가능한 프레임워크를 자유롭게 선택할 수 있습니다.
|
||||
* **데스크톱 환경**: 반응형 디자인은 구현하지 않으며, 데스크톱 Chrome 브라우저에서만 동작하면 됩니다.
|
||||
* **이미지 업로드**: 상품 이미지는 JPG, PNG 형식만 허용하며, 파일 크기는 **5MB** 이하로 제한됩니다. 이 검사는 **서버 측**에서 수행해야 합니다.
|
||||
* **데이터베이스**: 카테고리는 별도 테이블(`categories`)로 관리하고, 상품 테이블(`products`)은 `category_id`를 외래 키(FK)로 참조하는 구조로 설계하십시오.
|
||||
* **초기 데이터**: 애플리케이션 최초 실행 시(Seeder 또는 Migration) 다음 데이터가 자동으로 생성되어야 합니다.
|
||||
* 관리자 계정: 이름 `Admin`, 이메일 `admin@shoppress.local`, 비밀번호 `password` (비밀번호는 반드시 해시하여 저장)
|
||||
* 초기 카테고리 3개 이상 (예: `Clothing`, `Electronics`, `Food`)
|
||||
|
||||
---
|
||||
|
||||
## 3. 과제 요구사항 (Tasks to Complete)
|
||||
|
||||
### **1. 인증 (Authentication)**
|
||||
|
||||
#### 1.1 로그인 및 세션 관리
|
||||
|
||||
로그인 페이지는 `/admin/login`에서 접근 가능합니다. 사용자는 이메일과 비밀번호를 입력합니다. 로그인 성공 시 관리자 대시보드로 이동합니다. 로그인 실패 시 **"Incorrect email or password."** 메시지를 표시합니다.
|
||||
|
||||
`/admin` 하위 경로에 로그인 없이 접근하면 로그인 페이지로 리다이렉트됩니다. 로그아웃 시 세션이 파기되고 로그인 페이지로 이동합니다.
|
||||
|
||||
현재 로그인한 관리자 이름은 모든 관리자 페이지의 내비게이션에 표시되어야 합니다.
|
||||
|
||||
---
|
||||
|
||||
### **2. 관리자 기능 (Admin Features)**
|
||||
|
||||
#### 2.1 대시보드
|
||||
|
||||
대시보드는 관리자 패널의 기본 페이지(`/admin` 또는 `/admin/dashboard`)이며, 로그인 후 첫 번째로 표시되는 페이지입니다.
|
||||
|
||||
대시보드에는 다음 4가지 통계를 카드 형태로 표시하십시오.
|
||||
|
||||
| 카드 레이블 | 내용 |
|
||||
| :--- | :--- |
|
||||
| **Total Products** | 전체 상품 수 |
|
||||
| **Active Products** | 상태가 `Active`인 상품 수 |
|
||||
| **Total Orders** | 전체 주문 수 |
|
||||
| **Total Revenue** | 전체 누적 주문 금액(쿠폰 할인 적용 후 최종 결제 금액 합산 기준) |
|
||||
|
||||
또한 가장 최근 접수된 주문 5건의 목록을 표시하십시오. 각 항목에는 Order #(주문번호), Customer(구매자명), Total(최종 금액), Status(주문상태), Date(주문일시)가 포함되어야 합니다.
|
||||
|
||||
#### 2.2 상품 관리
|
||||
|
||||
상품 관리 페이지(`/admin/products`)는 전체 상품 목록을 테이블로 표시합니다.
|
||||
|
||||
테이블 컬럼: **Name**, **Category**, **Price**, **Stock**, **Status**, **Created At**, Edit / Delete buttons.
|
||||
|
||||
상품명을 기준으로 검색할 수 있는 검색창을 제공하십시오. 검색어를 입력하고 제출하면 해당 키워드가 포함된 상품만 목록에 표시됩니다.
|
||||
|
||||
**상품 등록 폼** (`/admin/products/new`)과 **상품 수정 폼** (`/admin/products/{id}/edit`)은 다음 필드를 포함해야 합니다.
|
||||
|
||||
* **Name** (필수, 최대 100자)
|
||||
* **Category** (필수, 드롭다운 선택 — DB에 등록된 카테고리 목록으로 구성)
|
||||
* **Price** (필수, 정수, 0 이상)
|
||||
* **Stock** (필수, 정수, 0 이상)
|
||||
* **Description** (`<textarea>`, 선택 항목)
|
||||
* **Image** (파일 업로드, JPG/PNG, 5MB 이하, **선택 항목** — 이미지 없이도 상품 등록 가능)
|
||||
* **Status** (드롭다운: `Active` / `Out of Stock` / `Hidden`)
|
||||
|
||||
수정 폼 진입 시 기존 데이터가 각 필드에 미리 채워져야 합니다. 이미지 파일을 새로 업로드하지 않으면 기존 이미지를 유지합니다. 새 이미지를 업로드하면 기존 이미지 파일을 서버에서 삭제하고 새 이미지로 교체합니다.
|
||||
|
||||
상품 삭제 시 **"Are you sure you want to delete this product?"** 확인 메시지를 표시합니다. 확인 시 해당 상품이 포함된 주문(`order_items`)이 1건 이상 존재하면 삭제를 거부하고 **"Cannot delete a product with order history."** 오류 메시지를 표시합니다. 주문 이력이 없는 상품은 DB에서 삭제하고 대표 이미지 파일도 서버에서 함께 삭제합니다.
|
||||
|
||||
#### 2.3 카테고리 관리
|
||||
|
||||
카테고리 관리 페이지(`/admin/categories`)에서 카테고리를 추가/수정/삭제할 수 있습니다.
|
||||
|
||||
* **추가**: 카테고리 이름을 입력하고 저장합니다. 이름이 중복될 경우 **"Category name already exists."** 오류 메시지를 표시합니다.
|
||||
* **수정**: 카테고리 이름을 변경할 수 있습니다. `categories` 테이블의 해당 행 이름만 변경하면 FK로 연결된 상품에 자동으로 반영됩니다.
|
||||
* **삭제**: 해당 카테고리를 참조하는 상품이 1개 이상 존재할 경우 삭제를 거부하고 **"Cannot delete a category that has products assigned."** 오류 메시지를 표시합니다.
|
||||
|
||||
#### 2.4 쿠폰 관리
|
||||
|
||||
쿠폰 관리 페이지(`/admin/coupons`)에서 할인 쿠폰을 등록하고 관리할 수 있습니다.
|
||||
|
||||
**쿠폰 등록 폼**은 다음 필드를 포함합니다.
|
||||
|
||||
* **Coupon Code** (필수, 영문 대문자 또는 숫자로만 구성, 6~12자, 중복 불가)
|
||||
* **Discount Type** (드롭다운: `Fixed` / `Percent`)
|
||||
* **Discount Value** (필수, 정수, 1 이상. `Percent` 선택 시 1~100 사이)
|
||||
* **Minimum Order Amount** (필수, 정수, 0 이상. 이 금액 미만의 주문에는 적용 불가)
|
||||
* **Expires At** (필수, `<input type="date">`. 오늘 날짜 이후만 선택 가능)
|
||||
* **Usage Limit** (필수, 정수, 1 이상)
|
||||
|
||||
쿠폰 목록 테이블 컬럼: **Code**, **Type**, **Value**, **Min. Order**, **Expires At**, **Remaining Uses**, Delete button. 잔여 사용 횟수는 `remaining_uses` 컬럼으로 관리하며, 등록 시 입력한 Usage Limit 값으로 초기화하고 쿠폰이 사용될 때마다 1씩 차감합니다.
|
||||
|
||||
#### 2.5 주문 관리
|
||||
|
||||
주문 관리 페이지(`/admin/orders`)는 전체 주문 목록을 테이블로 표시합니다.
|
||||
|
||||
테이블 컬럼: **Order #**, **Customer**, **Phone**, **Total**, **Status**, **Date**.
|
||||
|
||||
주문 상태 기준으로 필터링하는 드롭다운을 제공하십시오. 상태값은 `Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`이며, 선택한 상태의 주문만 목록에 표시됩니다.
|
||||
|
||||
주문 상세 페이지(`/admin/orders/{id}`)에서는 다음 정보를 확인할 수 있어야 합니다.
|
||||
|
||||
* Customer Info: 이름(Name), 연락처(Phone), 배송 주소(Shipping Address)
|
||||
* Order Items: 상품명(Product), 단가(Unit Price), 수량(Qty), 소계(Subtotal)
|
||||
* Coupon: 적용된 쿠폰 코드 및 할인 금액. 쿠폰 미적용 시 **"None"** 표시
|
||||
* Total: 최종 결제 금액
|
||||
* **Order Status** 변경 셀렉트박스(`Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`) 및 Save 버튼. 저장 성공 시 동일 페이지에 **"Order status updated."** 메시지를 표시합니다.
|
||||
* 주문 상태가 **`Cancelled`**로 변경될 때 해당 주문에 포함된 상품의 재고를 주문 수량만큼 복원합니다. 이미 `Cancelled` 상태인 주문에서 다시 `Cancelled`로 변경하는 경우 재고 복원은 수행하지 않습니다.
|
||||
|
||||
---
|
||||
|
||||
### **3. 공개 페이지 (Public Pages)**
|
||||
|
||||
#### 3.1 홈 — 상품 목록
|
||||
|
||||
홈(`/`)은 상태가 **`Active`**인 상품 목록을 카드 형태로 표시합니다. 각 카드에는 대표 이미지(없는 경우 회색 placeholder 영역 표시), 상품명, 카테고리명, 가격이 표시됩니다.
|
||||
|
||||
한 페이지에 **12개**씩 표시하며 페이지네이션을 제공합니다. 최근 등록 순으로 정렬합니다.
|
||||
|
||||
카테고리 필터 링크를 제공하며, 특정 카테고리 클릭 시 해당 카테고리의 **`Active`** 상품만 표시됩니다. 페이지네이션은 현재 카테고리 필터를 유지합니다.
|
||||
|
||||
키워드 검색창을 제공하며, 상품명 기준으로 검색됩니다. 카테고리 필터와 검색이 동시에 적용됩니다. 검색 상태에서 페이지네이션도 검색 조건을 유지합니다.
|
||||
|
||||
#### 3.2 상품 상세
|
||||
|
||||
상품 상세 페이지는 `/products/{id}`에서 접근 가능합니다. 존재하지 않는 ID 또는 상태가 **`Hidden`**인 상품에 접근하면 404 페이지를 반환합니다.
|
||||
|
||||
상품 상세 페이지에는 대표 이미지(없는 경우 회색 placeholder 영역 표시), 상품명, 카테고리명, 가격, 재고 수량, 상품 설명이 표시됩니다.
|
||||
|
||||
재고가 0이거나 상태가 **`Out of Stock`**인 상품은 **"Out of Stock"** 표시와 함께 구매 버튼이 비활성화됩니다.
|
||||
|
||||
재고가 1 이상이고 상태가 **`Active`**인 상품에는 수량 입력(`<input type="number">`, 최솟값 1, 최댓값은 현재 재고 수량)과 **"Buy Now"** 버튼이 표시됩니다. 버튼 클릭 시 선택한 상품 ID와 수량을 쿼리스트링으로 전달하여 주문 접수 페이지(`/checkout?product_id={id}&quantity={n}`)로 이동합니다.
|
||||
|
||||
#### 3.3 주문 접수
|
||||
|
||||
주문 접수 페이지(`/checkout`)는 쿼리스트링의 `product_id`와 `quantity`를 읽어 주문 대상 상품 정보(상품명, 단가, 수량, 소계)를 화면에 표시합니다. 서버에서 다음 조건을 확인하고, 하나라도 해당하면 홈(`/`)으로 리다이렉트합니다.
|
||||
|
||||
* `product_id`에 해당하는 상품이 존재하지 않는 경우
|
||||
* 상품 상태가 **`Hidden`** 또는 **`Out of Stock`**인 경우
|
||||
* 재고가 0인 경우
|
||||
* `quantity`가 1 미만이거나 현재 재고를 초과하는 경우
|
||||
|
||||
구매자 이름(Name), 연락처(Phone), 배송 주소(Shipping Address)를 입력하는 폼이 있습니다. 모든 필드는 필수이며 **서버 측 유효성 검사**를 통과한 데이터만 주문이 접수됩니다.
|
||||
|
||||
**쿠폰 코드 입력란(Coupon Code)**과 **"Apply"** 버튼이 있습니다. "Apply" 버튼 클릭 시 `fetch`를 사용하여 `/checkout/coupon` 엔드포인트에 아래 형식으로 POST 요청을 보내고, 그 결과를 페이지 새로고침 없이 화면에 반영합니다. 해당 엔드포인트는 선수가 직접 구현해야 합니다.
|
||||
|
||||
* 요청 Body (JSON): `{ "coupon_code": "ABCD1234", "subtotal": 30000 }`
|
||||
* 성공 응답 (JSON): `{ "valid": true, "discount_type": "fixed"|"percent", "discount_value": 3000, "discount_amount": 3000, "final_amount": 27000 }`
|
||||
* 실패 응답 (JSON): `{ "valid": false, "message": "error message" }`
|
||||
|
||||
* 유효한 쿠폰: 할인 금액을 계산하여 소계(Subtotal), 할인 금액(Discount), 최종 결제 금액(Total)을 화면에 표시합니다.
|
||||
* `Fixed` 할인: 소계에서 할인 값을 직접 차감합니다.
|
||||
* `Percent` 할인: `소계 × 할인율 / 100`을 차감합니다. 소수점 이하는 버림(floor) 처리합니다.
|
||||
* 최종 결제 금액은 0 미만이 되지 않습니다.
|
||||
* 유효하지 않은 쿠폰: 각 상황에 맞는 오류 메시지를 표시합니다.
|
||||
* 존재하지 않는 코드: **"Invalid coupon code."**
|
||||
* 만료된 쿠폰: **"This coupon has expired."**
|
||||
* 잔여 횟수 소진: **"This coupon has reached its usage limit."**
|
||||
* 최소 주문금액 미달: **"This coupon requires a minimum order of {n}."**
|
||||
|
||||
주문 접수 시 다음을 **하나의 트랜잭션**으로 처리하십시오.
|
||||
1. `orders` 테이블에 주문 정보(구매자명, 연락처, 배송주소, 적용된 쿠폰 ID — 없으면 NULL, 할인 금액 — 없으면 0, 최종 결제 금액)를 저장합니다.
|
||||
2. `order_items` 테이블에 주문 상품 정보(주문 ID, 상품 ID, **상품명 스냅샷**, 수량, 단가)를 저장합니다. 상품명은 주문 시점의 이름을 그대로 저장하여 이후 상품이 삭제되거나 이름이 변경되어도 주문 내역에서 올바르게 표시되도록 합니다.
|
||||
3. 해당 상품의 재고(`stock`)를 주문 수량만큼 차감합니다.
|
||||
4. 쿠폰이 적용된 경우 해당 쿠폰의 `remaining_uses`를 1 차감합니다.
|
||||
|
||||
재고가 요청 수량보다 부족할 경우 트랜잭션을 롤백하고 **"Not enough stock."** 오류 메시지를 표시합니다.
|
||||
|
||||
주문 접수 성공 시 주문 완료 페이지(`/orders/{id}/complete`)로 이동합니다. 주문 완료 페이지는 별도 인증 없이 접근 가능하며, 주문번호(Order #), 주문 상품 내역, 할인 금액(Discount), 최종 결제 금액(Total)을 표시합니다.
|
||||
|
||||
#### 3.4 레이아웃
|
||||
|
||||
모든 공개 페이지에는 헤더와 푸터가 포함됩니다. 헤더에는 사이트명(**ShopPress**)과 홈으로 이동하는 링크가 포함됩니다. 각 페이지의 내용을 반영한 `<title>` 태그가 적절히 설정되어야 합니다.
|
||||
|
||||
관리자 패널 페이지에는 사이드바 또는 상단 내비게이션이 포함됩니다. 내비게이션 메뉴 항목: **Dashboard**, **Products**, **Categories**, **Coupons**, **Orders**.
|
||||
|
||||
---
|
||||
|
||||
## 4. 제출 안내 (Submission Guidelines)
|
||||
|
||||
선수는 작업 결과물을 서버 디렉토리 `/module_b/`에 업로드하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 5. 채점 기준 요약 (Marking Scheme — Total 25점)
|
||||
|
||||
| 항목 | 세부 기준 | 배점 |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. 인증** | 로그인/로그아웃, 세션 보호, 미인증 접근 리다이렉트 | 2 |
|
||||
| **B. 대시보드** | 통계 4종 카드(영문 레이블), 최근 주문 5건 목록 | 1 |
|
||||
| **C. 상품 관리** | 목록(검색 포함), 등록(이미지 선택), 수정(이미지 유지/교체+기존 삭제), 삭제(주문 이력 있는 경우 거부 + 이미지 파일 삭제) | 5 |
|
||||
| **D. 카테고리 관리** | 추가(중복 검사), 수정, 삭제(참조 상품 존재 시 거부) | 2 |
|
||||
| **E. 쿠폰 관리** | 쿠폰 등록(전체 유효성 검사 포함), 목록 조회(잔여 횟수 표시), 삭제 | 3 |
|
||||
| **F. 주문 관리** | 목록(상태 필터), 상세 조회, 주문 상태 변경, Cancelled 시 재고 복원(중복 복원 방지) | 4 |
|
||||
| **G. 공개 — 상품 목록** | Active 상품 필터링, 카테고리 필터, 키워드 검색, 페이지네이션(조건 유지) | 3 |
|
||||
| **H. 공개 — 상품 상세** | 상품 정보 표시, Out of Stock 처리(재고 0 또는 Out of Stock 상태), 수량 입력 및 checkout 이동 | 1 |
|
||||
| **I. 공개 — 주문 접수** | checkout 진입 유효성(Out of Stock/Hidden/재고 초과 포함), 쿠폰 적용(fetch + 할인 계산), 트랜잭션(주문저장+재고차감+쿠폰차감), 완료 페이지 | 3 |
|
||||
| **J. 유효성 검사** | 서버 측 이미지 용량/확장자 검사, 주문 폼 필드 검사 | 1 |
|
||||
| **합계** | | **25** |
|
||||
0
module-c/.gitkeep
Normal file
81
module-c/module-c-en.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 🎮 Test Project: Module C - CyberTyper 2026
|
||||
|
||||
## 1. Project Overview
|
||||
The objective of this task is to develop a high-performance arcade game based on English typing. Competitors must analyze the provided `words_data.json` file to design a **Database (MySQL)** and complete a dynamic front-end engine that tracks real-time coordinates to destroy words by integrating it with an API.
|
||||
|
||||
## 2. Technical Definitions
|
||||
* **VFX (Visual Effects)**: Visual special effects within the game. This refers to effects such as letters breaking into fragments upon bullet collision.
|
||||
* **WPM (Words Per Minute)**: An index of typing speed. (Calculation: `Total words typed / Play time in minutes`)
|
||||
* **HP (Health Point)**: The player's vitality (starting at 100%). It decreases when words are missed, and the game ends when it reaches 0%.
|
||||
|
||||
## 3. Step 1: Backend API and Database Design (Server-side)
|
||||
All API endpoints must use the `/module_c/api/` path, and data persistence must be guaranteed.
|
||||
|
||||
### **A. Database Configuration (Mandatory)**
|
||||
* **Word Table**: You must create a table that stores the word text, difficulty level, and unique points based on the contents of the provided `words_data.json`.
|
||||
* **Ranking Table**: You must design a table to store player names, final scores, and max combos. This data must be maintained even after a server restart.
|
||||
|
||||
### **B. API Logic Requirements**
|
||||
* **Word Supply API (`GET /module_c/api/words?level=n`)**:
|
||||
1. Randomly extract words corresponding to the requested difficulty (`level`).
|
||||
2. **Duplicate Prevention**: Words already appearing on the current game screen must not be duplicated in the API call.
|
||||
* **Ranking API**: Records must be stored in the DB via `POST` requests, and the **Global TOP 5** records must be returned in descending order of score upon `GET` requests.
|
||||
|
||||
## 4. Step 2: Game Engine and Scoring Logic (Front-end Development)
|
||||
|
||||
### **A. Difficulty Settings (Spawn Interval & Fall Speed)**
|
||||
Words are generated at **random horizontal positions** at the top of the screen (y=0) and move downwards.
|
||||
|
||||
| Difficulty (Level) | Spawn Interval | Fall Speed | Multiplier ($Multiplier$) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Level 1** | Every 2.0s | Slow (50px/sec) | 1.0 |
|
||||
| **Level 2** | Every 1.5s | Normal (80px/sec) | 1.5 |
|
||||
| **Level 3** | Every 1.0s | Fast (120px/sec) | 2.0 |
|
||||
|
||||
### **B. Detailed Scoring Formula ($FinalScore$)**
|
||||
$$FinalScore = BasePoints + (CurrentCombo \times LevelMultiplier)$$
|
||||
* **$BasePoints$**: The unique points assigned to the matched word in the DB.
|
||||
* **$CurrentCombo$**: The number of consecutive successes. (Increments by 1 on success; resets to 0 immediately if a word hits the bottom).
|
||||
* **Display**: The calculated score must be displayed as a real-time cumulative value in the **Score** area of the game screen.
|
||||
|
||||
### **C. HP System and Recovery**
|
||||
* **Deduction**: HP decreases by **10%** whenever a word hits the bottom line.
|
||||
* **Recovery**: Every time a **20 Combo** is achieved, **5%** of the currently lost HP is restored as a bonus.
|
||||
|
||||
## 5. Step 3: UI/UX and Scene Specifications (Design Implementation)
|
||||
|
||||
### **A. 3-Scene SPA (Single Page Application) Structure**
|
||||
* This task must be developed as an **SPA structure** where screens transition without a full page refresh using JavaScript.
|
||||
* **Scene 1 (Start)**: Player name entry and difficulty selection.
|
||||
* **Scene 2 (Play)**: The actual game progression screen.
|
||||
* **Scene 3 (Result)**: Final score summary and Global TOP 5 ranking display.
|
||||
|
||||
### **B. Start Scene Requirements**
|
||||
* **Input Elements**: Game title (CyberTyper 2026), **Player Name input field (Required)**, **Difficulty Selection (Level 1, 2, 3)** via radio buttons or a select box.
|
||||
* **Start Button**: Becomes active only when all information is entered; clicking it enters the game screen.
|
||||
|
||||
### **C. Game Screen Mandatory Components (Marking Items)**
|
||||
* **Score**: Real-time cumulative scoreboard.
|
||||
* **Combo**: Current consecutive success count.
|
||||
* **WPM**: Real-time calculated words per minute.
|
||||
* **HP Gauge**: A visually changing health bar.
|
||||
* **Input Field**: Text area for the player to type English words.
|
||||
* **Falling Words**: Words falling from random top positions.
|
||||
|
||||
### **D. Projectile and Fever Effects**
|
||||
* **Bullet and Collision**: Upon pressing Enter, a bullet is fired from the bottom toward the **real-time coordinates** of the target word. When a collision occurs, a letter-fragment VFX is generated and the word is destroyed.
|
||||
* **Fever Mode**: Achieving a 20 Combo reduces the fall speed by 50% for 5 seconds and activates a neon background effect.
|
||||
|
||||
### **E. Ranking Scene (Result Scene)**
|
||||
* Displays the **Global TOP 5** leaderboard retrieved from the database.
|
||||
* **[New Game]** Button: Clicking this resets all settings and returns to the Name Input screen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Marking Scheme Summary (Total 25.00)
|
||||
|
||||
| Category | Detailed Criteria | Marks |
|
||||
| :--- | :--- | :--- |
|
||||
| **Design (7.0)** | Dynamic projectile/particle effects, visibility of essential UI (HP, Score), and VFX completion. | 7.0 |
|
||||
| **Front-end (11.0)** | Difficulty-specific logic (Interval/Speed), duplicate-free word management, and bullet coordinate calculation. | 11.0 |
|
||||
| **Back-end (7.0)** | **DB table design and data persistence**, Global TOP 5 Ranking API design. | 7.0 |
|
||||
84
module-c/module-c-kr.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 🎮 Test Project: Module C - CyberTyper 2026
|
||||
|
||||
## 1. 프로젝트 개요 (Project Overview)
|
||||
본 과제는 영문 타이핑을 기반으로 한 고성능 아케이드 게임 개발입니다. 선수는 제공된 `words_data.json` 파일을 분석하여 **데이터베이스(MySQL)**를 설계하고 , 탄환이 실시간 좌표를 추적하여 단어를 파괴하는 역동적인 프론트엔드 엔진을 API와 연동하여 완성해야 합니다.
|
||||
|
||||
## 2. 주요 용어 정의 (Technical Definitions)
|
||||
* **VFX (Visual Effects)**: 게임 내 시각 특수 효과입니다. 탄환 충돌 시 글자가 파편으로 부서지는 효과 등을 의미합니다.
|
||||
* **WPM (Words Per Minute)**: 분당 단어 입력 속도 지표입니다. (계산식: `타이핑한 총 단어 수 / 플레이 시간(분)`)
|
||||
* **HP (Health Point)**: 플레이어의 생명력(100%)이며, 단어를 놓칠 때마다 차감되어 0%가 되면 게임이 종료됩니다.
|
||||
|
||||
## 3. Step 1: 백엔드 API 및 데이터베이스 설계 (Server-side)
|
||||
모든 API 엔드포인트는 `/module_c/api/` 경로를 사용하며 , 데이터의 영속성(Persistence)을 보장해야 합니다.
|
||||
|
||||
### **A. 데이터베이스 구성 (필수 구현)**
|
||||
* **Word Table**: 제공된 `words_data.json`의 내용을 기반으로 단어 텍스트, 난이도 레벨, 고유 점수 정보를 저장하는 테이블을 생성해야 합니다.
|
||||
* **Ranking Table**: 플레이어 이름, 최종 점수, 최대 콤보를 저장하는 테이블을 설계해야 합니다. 이 데이터는 서버 재시작 후에도 유지되어야 합니다.
|
||||
|
||||
### **B. API 로직 요구사항**
|
||||
* **단어 공급 API (`GET /module_c/api/words?level=n`)**:
|
||||
1. 요청한 난이도(`level`)에 해당하는 단어 중 랜덤하게 추출합니다.
|
||||
2. **중복 방지**: 현재 게임 화면에 이미 나타나 있는 단어는 중복해서 호출되지 않도록 처리해야 합니다.
|
||||
* **랭킹 API**: `POST` 요청을 통해 기록을 DB에 저장하고, `GET` 요청 시 상위 **Global TOP 5** 기록을 점수 내림차순으로 반환합니다.
|
||||
|
||||
### **C. 제출물 (Deliverables)**
|
||||
* 선수는 작성한 API 소스코드와 함께, 설계한 데이터베이스 구조를 확인할 수 있는 **`table.sql`** 파일을 반드시 루트 폴더에 포함해야 합니다.
|
||||
|
||||
## 4. Step 2: 게임 엔진 및 점수 로직 (Front-end Development)
|
||||
|
||||
### **A. 난이도별 설정 (생성 주기 및 낙하 속도)**
|
||||
단어는 화면 상단(y=0)의 **가로축 랜덤 위치**에서 생성되어 아래로 이동합니다.
|
||||
|
||||
| 난이도 (Level) | 단어 생성 주기 (Interval) | 낙하 속도 (Speed) | 난이도 가중치 ($Multiplier$) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Level 1** | 2.0초마다 1개 생성 | 느림 (50px/sec) | 1.0 |
|
||||
| **Level 2** | 1.5초마다 1개 생성 | 보통 (80px/sec) | 1.5 |
|
||||
| **Level 3** | 1.0초마다 1개 생성 | 빠름 (120px/sec) | 2.0 |
|
||||
|
||||
### **B. 상세 점수 산출 공식 ($FinalScore$)**
|
||||
$$FinalScore = BasePoints + (CurrentCombo \times LevelMultiplier)$$
|
||||
* **$BasePoints$ (고유 점수)**: 맞춘 단어가 DB에서 가지고 있는 고유 점수 값입니다.
|
||||
* **$CurrentCombo$**: 연속 성공 횟수입니다. (성공 시 +1, 단어가 바닥에 닿으면 즉시 0으로 초기화).
|
||||
* **표시**: 계산된 점수는 게임 화면 **Score** 영역에 실시간 누적 표시됩니다.
|
||||
|
||||
### **C. HP 시스템 및 회복**
|
||||
* **감소**: 단어가 바닥(Bottom Line)에 닿으면 HP가 **10%** 차감됩니다.
|
||||
* **회복**: **20 콤보** 달성 시마다 현재 줄어든 HP의 **5%**가 보너스로 즉시 회복됩니다.
|
||||
|
||||
## 5. Step 3: UI/UX 및 장면별 명세 (Design Implementation)
|
||||
|
||||
### **A. 3-Scene SPA(Single Page Application) 구조**
|
||||
* 본 과제는 페이지 전체 새로고침 없이 자바스크립트를 통해 화면이 전환되는 **SPA 구조**로 개발되어야 합니다.
|
||||
* **Scene 1 (Start)**: 플레이어 이름 입력 및 난이도 선택.
|
||||
* **Scene 2 (Play)**: 실제 게임 진행 화면.
|
||||
* **Scene 3 (Result)**: 최종 점수 요약 및 Global TOP 5 랭킹 출력 화면.
|
||||
|
||||
### **B. 시작 화면 (Start Scene)**
|
||||
* **입력 요소**: 게임 제목(CyberTyper 2026), **플레이어 이름 입력란(필수)**, **난이도 선택(Level 1, 2, 3)** 라디오 버튼 또는 선택창.
|
||||
* **시작 버튼**: 모든 정보가 입력되면 활성화되며, 클릭 시 게임 화면으로 진입합니다.
|
||||
|
||||
### **C. 게임 화면 필수 구성 요소 (채점 항목)**
|
||||
* **Score**: 실시간 누적 점수판.
|
||||
* **Combo**: 현재 연속 성공 횟수.
|
||||
* **WPM**: 실시간으로 계산되는 분당 타수.
|
||||
* **HP Gauge**: 시각적으로 변하는 생명력 바.
|
||||
* **Input Field**: 플레이어가 영문 단어를 입력하는 텍스트 영역.
|
||||
* **Falling Words**: 상단 랜덤 위치에서 낙하하는 단어들.
|
||||
|
||||
### **D. 투사체(Projectile) 및 피버 효과**
|
||||
* **탄환 및 충돌**: 엔터 입력 시 하단에서 단어의 **실시간 좌표**로 탄환이 발사되며, 충돌 시 글자 파편 VFX가 발생하며 단어가 소멸합니다.
|
||||
* **Fever Mode 시각 효과**: 20 콤보 달성 시 화면 중앙에 **"FEVER MODE"** 텍스트 애니메이션이 잠시 나타났다가 사라져야 하며, 배경의 네온 VFX는 기존 배경과 시각적으로 확연히 구분되어야 합니다.
|
||||
|
||||
### **E. 랭킹 화면 (Result Scene)**
|
||||
* 데이터베이스에서 불러온 **Global TOP 5** 순위표를 표시합니다. 순위표에는 1등부터 5등까지의 순위와 이름, 점수, 순위달성일시가 표시되어야 합니다.
|
||||
* **[New Game]** 버튼 클릭 시 모든 설정을 초기화하고 이름 입력 화면으로 되돌아갑니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 채점 기준 요약 (Marking Scheme - 25.00)
|
||||
|
||||
| 항목 | 상세 기준 | 배점 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Design (7.0)** | 탄환/파편 역동성, 필수 UI(HP, Score) 가시성 및 VFX 완성도 | 7.0 |
|
||||
| **Front-end (11.0)** | 난이도별 로직(주기/속도), 중복 없는 단어 관리, 탄환 좌표 연산 | 11.0 |
|
||||
| **Back-end (7.0)** | **DB 테이블 설계 및 데이터 영속성**, TOP 5 랭킹 API 설계 | 7.0 |
|
||||
81
module-c/module-c-uz.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 🎮 Test Project: Module C - CyberTyper 2026
|
||||
|
||||
## 1. Loyiha sharhi (Project Overview)
|
||||
Ushbu topshiriqning maqsadi ingliz tili bo'yicha yuqori mahsuldorlikka ega arkada o'yinini ishlab chiqishdir. Ishtirokchilar taqdim etilgan `words_data.json` faylini tahlil qilib, **Ma'lumotlar bazasi (MySQL)**ni loyihalashlari hamda so'zlarni yo'q qilish uchun real vaqt rejimida koordinatalarni kuzatuvchi dinamik front-end dvigatelini API bilan integratsiya qilgan holda yakunlashlari kerak.
|
||||
|
||||
## 2. Texnik tushunchalar (Technical Definitions)
|
||||
* **VFX (Visual Effects)**: O'yindagi vizual maxsus effektlar. Bu o'q urilishi natijasida so'z harflarining parchalanib ketishi kabi effektlarni anglatadi.
|
||||
* **WPM (Words Per Minute)**: Daqiqasiga yozilgan so'zlar tezligi ko'rsatkichi. (Hisoblash: `Yozilgan umumiy so'zlar / O'yin vaqti (daqiqada)`)
|
||||
* **HP (Health Point)**: O'yinchining hayot darajasi (100% dan boshlanadi). So'zlar o'tkazib yuborilganda u kamayadi va 0% ga yetganda o'yin yakunlanadi.
|
||||
|
||||
## 3. 1-qadam: Backend API va ma'lumotlar bazasi dizayni (Server-side)
|
||||
Barcha API nuqtalari (endpoints) `/module_c/api/` yo'lidan foydalanishi va ma'lumotlarning saqlanishi (persistence) kafolatlanishi kerak.
|
||||
|
||||
### **A. Ma'lumotlar bazasi konfiguratsiyasi (Majburiy)**
|
||||
* **Word Table**: Taqdim etilgan `words_data.json` fayli asosida so'z matni, qiyinchilik darajasi va unikal ballarni saqlaydigan jadval yaratishingiz kerak.
|
||||
* **Ranking Table**: O'yinchi ismlari, yakuniy ballar va maksimal kombolarni saqlash uchun jadval loyihalashingiz kerak. Ushbu ma'lumotlar server qayta ishga tushirilgandan keyin ham saqlanib qolishi shart.
|
||||
|
||||
### **B. API mantiqiy talablari**
|
||||
* **So'zlarni yetkazib berish API (`GET /module_c/api/words?level=n`)**:
|
||||
1. So'ralgan qiyinchilik darajasiga (`level`) mos keladigan so'zlarni tasodifiy tarzda chiqarish.
|
||||
2. **Takrorlanishning oldini olish**: Hozirgi o'yin ekranida mavjud bo'lgan so'zlar API so'rovida qayta takrorlanmasligi kerak.
|
||||
* **Ranking API**: Rekordlar `POST` so'rovlari orqali MBda saqlanishi va `GET` so'rovlari orqali ballarning kamayish tartibida **Global TOP 5** rekordlari qaytarilishi kerak.
|
||||
|
||||
## 4. 2-qadam: O'yin dvigateli va ballar hisobi mantiqi (Front-end Development)
|
||||
|
||||
### **A. Qiyinchilik darajasi sozlamalari (Yaratilish oralig'i va tushish tezligi)**
|
||||
So'zlar ekranning yuqori qismida (y=0) **tasodifiy gorizontal pozitsiyalarda** yaratiladi va pastga qarab harakatlanadi.
|
||||
|
||||
| Qiyinchilik (Daraja) | Yaratilish oralig'i | Tushish tezligi | Ko'paytiruvchi ($Multiplier$) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Level 1** | Har 2.0s da | Sekin (50px/sek) | 1.0 |
|
||||
| **Level 2** | Har 1.5s da | O'rta (80px/sek) | 1.5 |
|
||||
| **Level 3** | Har 1.0s da | Tez (120px/sek) | 2.0 |
|
||||
|
||||
### **B. Ballarni hisoblashning batafsil formulasi ($FinalScore$)**
|
||||
$$FinalScore = BasePoints + (CurrentCombo \times LevelMultiplier)$$
|
||||
* **$BasePoints$**: MBdagi mos kelgan so'zga biriktirilgan unikal ball.
|
||||
* **$CurrentCombo$**: Ketma-ket muvaffaqiyatlar soni. (Muvaffaqiyatda 1 ga oshadi; so'z pastki qismga tegsa, darhol 0 ga qaytadi).
|
||||
* **Ko'rsatkich**: Hisoblangan ball o'yin ekranining **Score** qismida real vaqt rejimida yig'ilib borishi kerak.
|
||||
|
||||
### **C. HP tizimi va tiklanish**
|
||||
* **Kamayish**: So'z pastki chiziqqa tegsa, HP **10%** ga kamayadi.
|
||||
* **Tiklanish**: Har safar **20 Combo** ga erishilganda, yo'qotilgan HPning **5%** miqdori bonus sifatida tiklanadi.
|
||||
|
||||
## 5. 3-qadam: UI/UX va sahna spetsifikatsiyalari (Design Implementation)
|
||||
|
||||
### **A. 3-Scene SPA (Single Page Application) tuzilmasi**
|
||||
* Ushbu topshiriq JavaScript yordamida sahifani to'liq yangilamasdan almashadigan **SPA tuzilmasida** ishlab chiqilishi kerak.
|
||||
* **Scene 1 (Start)**: O'yinchi ismini kiritish va qiyinchilik darajasini tanlash.
|
||||
* **Scene 2 (Play)**: Haqiqiy o'yin jarayoni ekrani.
|
||||
* **Scene 3 (Result)**: Yakuniy ball xulosasi va Global TOP 5 reyting ekrani.
|
||||
|
||||
### **B. Boshlang'ich sahna talablari**
|
||||
* **Kiritish elementlari**: O'yin nomi (CyberTyper 2026), **O'yinchi ismi maydoni (Majburiy)**, radio-tugmalar yoki tanlash qutisi orqali **Qiyinchilikni tanlash (Level 1, 2, 3)**.
|
||||
* **Start tugmasi**: Barcha ma'lumotlar kiritilgandagina faollashadi; uni bosish o'yin ekraniga olib kiradi.
|
||||
|
||||
### **C. O'yin ekranining majburiy komponentlari (Baholash elementlari)**
|
||||
* **Score**: Real vaqt rejimidagi ballar jadvali.
|
||||
* **Combo**: Ketma-ket muvaffaqiyatli urishlar soni.
|
||||
* **WPM**: Real vaqtda hisoblangan daqiqalik so'zlar soni.
|
||||
* **HP Gauge**: Vizual tarzda o'zgarib turadigan hayot bari.
|
||||
* **Input Field**: O'yinchi inglizcha so'zlarni yozishi uchun matn maydoni.
|
||||
* **Falling Words**: Yuqoridan tasodifiy joylardan tushayotgan so'zlar.
|
||||
|
||||
### **D. Proyektil (o'q) va Fever effektlari**
|
||||
* **O'q va to'qnashuv**: Enter tugmasi bosilganda, pastdan maqsadli so'zning **real vaqt koordinatalari** tomon o'q otiladi. To'qnashuv sodir bo'lganda, harflar parchalanishi VFX hosil bo'ladi va so'z yo'q qilinadi.
|
||||
* **Fever Mode**: 20 ta komboga erishish tushish tezligini 5 soniya davomida 50% ga kamaytiradi va neon fon effektini faollashtiradi.
|
||||
|
||||
### **E. Reyting sahnasi (Natija sahifasi)**
|
||||
* Ma'lumotlar bazasidan olingan **Global TOP 5** yetakchilar jadvalini ko'rsatadi.
|
||||
* **[New Game]** tugmasi: Buni bosish barcha sozlamalarni tiklaydi va ism kiritish ekraniga qaytaradi.
|
||||
|
||||
---
|
||||
|
||||
## 6. Baholash sxemasi xulosasi (Jami 25.00)
|
||||
|
||||
| Toifa | Batafsil mezonlar | Ballar |
|
||||
| :--- | :--- | :--- |
|
||||
| **Design (7.0)** | Dinamik proyektil/zarracha effektlari, muhim UI elementlari (HP, Score) ko'rinishi va VFX sifati. | 7.0 |
|
||||
| **Front-end (11.0)** | Qiyinchilikka xos mantiq (Interval/Tezlik), takrorlanishsiz so'zlar boshqaruvi va o'q koordinatalarini hisoblash. | 11.0 |
|
||||
| **Back-end (7.0)** | **MB jadvali dizayni va ma'lumotlar barqarorligi**, Global TOP 5 reyting API dizayni. | 7.0 |
|
||||
303
module-c/words_data.json
Normal file
@@ -0,0 +1,303 @@
|
||||
[
|
||||
{ "word": "Apple", "level": 1, "points": 10 },
|
||||
{ "word": "Blue", "level": 1, "points": 10 },
|
||||
{ "word": "Cake", "level": 1, "points": 10 },
|
||||
{ "word": "Desk", "level": 1, "points": 10 },
|
||||
{ "word": "East", "level": 1, "points": 10 },
|
||||
{ "word": "Fish", "level": 1, "points": 10 },
|
||||
{ "word": "Golf", "level": 1, "points": 10 },
|
||||
{ "word": "Home", "level": 1, "points": 10 },
|
||||
{ "word": "Icon", "level": 1, "points": 10 },
|
||||
{ "word": "Jump", "level": 1, "points": 10 },
|
||||
{ "word": "King", "level": 1, "points": 10 },
|
||||
{ "word": "Lamp", "level": 1, "points": 10 },
|
||||
{ "word": "Moon", "level": 1, "points": 10 },
|
||||
{ "word": "Note", "level": 1, "points": 10 },
|
||||
{ "word": "Open", "level": 1, "points": 10 },
|
||||
{ "word": "Park", "level": 1, "points": 10 },
|
||||
{ "word": "Quiz", "level": 1, "points": 10 },
|
||||
{ "word": "Road", "level": 1, "points": 10 },
|
||||
{ "word": "Star", "level": 1, "points": 10 },
|
||||
{ "word": "Tree", "level": 1, "points": 10 },
|
||||
{ "word": "Unit", "level": 1, "points": 10 },
|
||||
{ "word": "View", "level": 1, "points": 10 },
|
||||
{ "word": "Wind", "level": 1, "points": 10 },
|
||||
{ "word": "Xray", "level": 1, "points": 10 },
|
||||
{ "word": "Yard", "level": 1, "points": 10 },
|
||||
{ "word": "Zero", "level": 1, "points": 10 },
|
||||
{ "word": "Bird", "level": 1, "points": 10 },
|
||||
{ "word": "Cold", "level": 1, "points": 10 },
|
||||
{ "word": "Door", "level": 1, "points": 10 },
|
||||
{ "word": "Edge", "level": 1, "points": 10 },
|
||||
{ "word": "Fire", "level": 1, "points": 10 },
|
||||
{ "word": "Gate", "level": 1, "points": 10 },
|
||||
{ "word": "Hill", "level": 1, "points": 10 },
|
||||
{ "word": "Ink", "level": 1, "points": 10 },
|
||||
{ "word": "Java", "level": 1, "points": 10 },
|
||||
{ "word": "Kite", "level": 1, "points": 10 },
|
||||
{ "word": "Leaf", "level": 1, "points": 10 },
|
||||
{ "word": "Mail", "level": 1, "points": 10 },
|
||||
{ "word": "Near", "level": 1, "points": 10 },
|
||||
{ "word": "Oil", "level": 1, "points": 10 },
|
||||
{ "word": "Page", "level": 1, "points": 10 },
|
||||
{ "word": "Rain", "level": 1, "points": 10 },
|
||||
{ "word": "Sand", "level": 1, "points": 10 },
|
||||
{ "word": "Time", "level": 1, "points": 10 },
|
||||
{ "word": "User", "level": 1, "points": 10 },
|
||||
{ "word": "Vase", "level": 1, "points": 10 },
|
||||
{ "word": "Wave", "level": 1, "points": 10 },
|
||||
{ "word": "Zone", "level": 1, "points": 10 },
|
||||
{ "word": "Area", "level": 1, "points": 10 },
|
||||
{ "word": "Best", "level": 1, "points": 10 },
|
||||
{ "word": "City", "level": 1, "points": 10 },
|
||||
{ "word": "Data", "level": 1, "points": 10 },
|
||||
{ "word": "Easy", "level": 1, "points": 10 },
|
||||
{ "word": "Fast", "level": 1, "points": 10 },
|
||||
{ "word": "Girl", "level": 1, "points": 10 },
|
||||
{ "word": "Hope", "level": 1, "points": 10 },
|
||||
{ "word": "Item", "level": 1, "points": 10 },
|
||||
{ "word": "Join", "level": 1, "points": 10 },
|
||||
{ "word": "Keep", "level": 1, "points": 10 },
|
||||
{ "word": "Line", "level": 1, "points": 10 },
|
||||
{ "word": "Mind", "level": 1, "points": 10 },
|
||||
{ "word": "Name", "level": 1, "points": 10 },
|
||||
{ "word": "Only", "level": 1, "points": 10 },
|
||||
{ "word": "Plan", "level": 1, "points": 10 },
|
||||
{ "word": "Real", "level": 1, "points": 10 },
|
||||
{ "word": "Side", "level": 1, "points": 10 },
|
||||
{ "word": "Text", "level": 1, "points": 10 },
|
||||
{ "word": "Upon", "level": 1, "points": 10 },
|
||||
{ "word": "Very", "level": 1, "points": 10 },
|
||||
{ "word": "Work", "level": 1, "points": 10 },
|
||||
{ "word": "Year", "level": 1, "points": 10 },
|
||||
{ "word": "Able", "level": 1, "points": 10 },
|
||||
{ "word": "Back", "level": 1, "points": 10 },
|
||||
{ "word": "Case", "level": 1, "points": 10 },
|
||||
{ "word": "Done", "level": 1, "points": 10 },
|
||||
{ "word": "Even", "level": 1, "points": 10 },
|
||||
{ "word": "Feel", "level": 1, "points": 10 },
|
||||
{ "word": "Give", "level": 1, "points": 10 },
|
||||
{ "word": "High", "level": 1, "points": 10 },
|
||||
{ "word": "Into", "level": 1, "points": 10 },
|
||||
{ "word": "Just", "level": 1, "points": 10 },
|
||||
{ "word": "Know", "level": 1, "points": 10 },
|
||||
{ "word": "Last", "level": 1, "points": 10 },
|
||||
{ "word": "Make", "level": 1, "points": 10 },
|
||||
{ "word": "Next", "level": 1, "points": 10 },
|
||||
{ "word": "Over", "level": 1, "points": 10 },
|
||||
{ "word": "Part", "level": 1, "points": 10 },
|
||||
{ "word": "Read", "level": 1, "points": 10 },
|
||||
{ "word": "Same", "level": 1, "points": 10 },
|
||||
{ "word": "Tell", "level": 1, "points": 10 },
|
||||
{ "word": "Used", "level": 1, "points": 10 },
|
||||
{ "word": "Vary", "level": 1, "points": 10 },
|
||||
{ "word": "Want", "level": 1, "points": 10 },
|
||||
{ "word": "Your", "level": 1, "points": 10 },
|
||||
{ "word": "Acid", "level": 1, "points": 10 },
|
||||
{ "word": "Bank", "level": 1, "points": 10 },
|
||||
{ "word": "Call", "level": 1, "points": 10 },
|
||||
{ "word": "Deep", "level": 1, "points": 10 },
|
||||
{ "word": "Each", "level": 1, "points": 10 },
|
||||
{ "word": "Face", "level": 1, "points": 10 },
|
||||
{ "word": "Action", "level": 2, "points": 20 },
|
||||
{ "word": "Bridge", "level": 2, "points": 20 },
|
||||
{ "word": "Camera", "level": 2, "points": 20 },
|
||||
{ "word": "Device", "level": 2, "points": 20 },
|
||||
{ "word": "Energy", "level": 2, "points": 20 },
|
||||
{ "word": "Future", "level": 2, "points": 20 },
|
||||
{ "word": "Garden", "level": 2, "points": 20 },
|
||||
{ "word": "Hammer", "level": 2, "points": 20 },
|
||||
{ "word": "Island", "level": 2, "points": 20 },
|
||||
{ "word": "Jungle", "level": 2, "points": 20 },
|
||||
{ "word": "Knight", "level": 2, "points": 20 },
|
||||
{ "word": "Laptop", "level": 2, "points": 20 },
|
||||
{ "word": "Market", "level": 2, "points": 20 },
|
||||
{ "word": "Number", "level": 2, "points": 20 },
|
||||
{ "word": "Object", "level": 2, "points": 20 },
|
||||
{ "word": "Player", "level": 2, "points": 20 },
|
||||
{ "word": "Quartz", "level": 2, "points": 20 },
|
||||
{ "word": "Rocket", "level": 2, "points": 20 },
|
||||
{ "word": "Screen", "level": 2, "points": 20 },
|
||||
{ "word": "Target", "level": 2, "points": 20 },
|
||||
{ "word": "Update", "level": 2, "points": 20 },
|
||||
{ "word": "Visual", "level": 2, "points": 20 },
|
||||
{ "word": "Window", "level": 2, "points": 20 },
|
||||
{ "word": "Yellow", "level": 2, "points": 20 },
|
||||
{ "word": "Zodiac", "level": 2, "points": 20 },
|
||||
{ "word": "Animal", "level": 2, "points": 20 },
|
||||
{ "word": "Battle", "level": 2, "points": 20 },
|
||||
{ "word": "Castle", "level": 2, "points": 20 },
|
||||
{ "word": "Desert", "level": 2, "points": 20 },
|
||||
{ "word": "Effect", "level": 2, "points": 20 },
|
||||
{ "word": "Flower", "level": 2, "points": 20 },
|
||||
{ "word": "Guitar", "level": 2, "points": 20 },
|
||||
{ "word": "Header", "level": 2, "points": 20 },
|
||||
{ "word": "Impact", "level": 2, "points": 20 },
|
||||
{ "word": "Junior", "level": 2, "points": 20 },
|
||||
{ "word": "Kernel", "level": 2, "points": 20 },
|
||||
{ "word": "Layout", "level": 2, "points": 20 },
|
||||
{ "word": "Method", "level": 2, "points": 20 },
|
||||
{ "word": "Native", "level": 2, "points": 20 },
|
||||
{ "word": "Output", "level": 2, "points": 20 },
|
||||
{ "word": "Public", "level": 2, "points": 20 },
|
||||
{ "word": "Return", "level": 2, "points": 20 },
|
||||
{ "word": "Silver", "level": 2, "points": 20 },
|
||||
{ "word": "Travel", "level": 2, "points": 20 },
|
||||
{ "word": "Useful", "level": 2, "points": 20 },
|
||||
{ "word": "Valley", "level": 2, "points": 20 },
|
||||
{ "word": "Worker", "level": 2, "points": 20 },
|
||||
{ "word": "Author", "level": 2, "points": 20 },
|
||||
{ "word": "Beauty", "level": 2, "points": 20 },
|
||||
{ "word": "Circle", "level": 2, "points": 20 },
|
||||
{ "word": "Driver", "level": 2, "points": 20 },
|
||||
{ "word": "Expert", "level": 2, "points": 20 },
|
||||
{ "word": "Friend", "level": 2, "points": 20 },
|
||||
{ "word": "Gender", "level": 2, "points": 20 },
|
||||
{ "word": "Health", "level": 2, "points": 20 },
|
||||
{ "word": "Income", "level": 2, "points": 20 },
|
||||
{ "word": "Leader", "level": 2, "points": 20 },
|
||||
{ "word": "Module", "level": 2, "points": 20 },
|
||||
{ "word": "Nature", "level": 2, "points": 20 },
|
||||
{ "word": "Option", "level": 2, "points": 20 },
|
||||
{ "word": "Policy", "level": 2, "points": 20 },
|
||||
{ "word": "Report", "level": 2, "points": 20 },
|
||||
{ "word": "Sector", "level": 2, "points": 20 },
|
||||
{ "word": "Theory", "level": 2, "points": 20 },
|
||||
{ "word": "Unique", "level": 2, "points": 20 },
|
||||
{ "word": "Volume", "level": 2, "points": 20 },
|
||||
{ "word": "Weight", "level": 2, "points": 20 },
|
||||
{ "word": "Advice", "level": 2, "points": 20 },
|
||||
{ "word": "Bottom", "level": 2, "points": 20 },
|
||||
{ "word": "Client", "level": 2, "points": 20 },
|
||||
{ "word": "Design", "level": 2, "points": 20 },
|
||||
{ "word": "Engine", "level": 2, "points": 20 },
|
||||
{ "word": "Family", "level": 2, "points": 20 },
|
||||
{ "word": "Growth", "level": 2, "points": 20 },
|
||||
{ "word": "Height", "level": 2, "points": 20 },
|
||||
{ "word": "Inside", "level": 2, "points": 20 },
|
||||
{ "word": "Memory", "level": 2, "points": 20 },
|
||||
{ "word": "Notice", "level": 2, "points": 20 },
|
||||
{ "word": "Office", "level": 2, "points": 20 },
|
||||
{ "word": "Period", "level": 2, "points": 20 },
|
||||
{ "word": "Result", "level": 2, "points": 20 },
|
||||
{ "word": "Series", "level": 2, "points": 20 },
|
||||
{ "word": "Source", "level": 2, "points": 20 },
|
||||
{ "word": "Update", "level": 2, "points": 20 },
|
||||
{ "word": "Window", "level": 2, "points": 20 },
|
||||
{ "word": "Agency", "level": 2, "points": 20 },
|
||||
{ "word": "Button", "level": 2, "points": 20 },
|
||||
{ "word": "Course", "level": 2, "points": 20 },
|
||||
{ "word": "Degree", "level": 2, "points": 20 },
|
||||
{ "word": "Entity", "level": 2, "points": 20 },
|
||||
{ "word": "Factor", "level": 2, "points": 20 },
|
||||
{ "word": "Global", "level": 2, "points": 20 },
|
||||
{ "word": "Hidden", "level": 2, "points": 20 },
|
||||
{ "word": "Involved", "level": 2, "points": 20 },
|
||||
{ "word": "Length", "level": 2, "points": 20 },
|
||||
{ "word": "Modern", "level": 2, "points": 20 },
|
||||
{ "word": "Object", "level": 2, "points": 20 },
|
||||
{ "word": "Parent", "level": 2, "points": 20 },
|
||||
{ "word": "Region", "level": 2, "points": 20 },
|
||||
{ "word": "Status", "level": 2, "points": 20 },
|
||||
{ "word": "Algorithm", "level": 3, "points": 40 },
|
||||
{ "word": "Blockchain", "level": 3, "points": 40 },
|
||||
{ "word": "Cybersecurity", "level": 3, "points": 40 },
|
||||
{ "word": "Development", "level": 3, "points": 40 },
|
||||
{ "word": "Environment", "level": 3, "points": 40 },
|
||||
{ "word": "Framework", "level": 3, "points": 40 },
|
||||
{ "word": "Generation", "level": 3, "points": 40 },
|
||||
{ "word": "Hierarchical", "level": 3, "points": 40 },
|
||||
{ "word": "Infrastructure", "level": 3, "points": 40 },
|
||||
{ "word": "JavaScript", "level": 3, "points": 40 },
|
||||
{ "word": "Knowledge", "level": 3, "points": 40 },
|
||||
{ "word": "Localization", "level": 3, "points": 40 },
|
||||
{ "word": "Maintenance", "level": 3, "points": 40 },
|
||||
{ "word": "Networking", "level": 3, "points": 40 },
|
||||
{ "word": "Optimization", "level": 3, "points": 40 },
|
||||
{ "word": "Programming", "level": 3, "points": 40 },
|
||||
{ "word": "Quantitative", "level": 3, "points": 40 },
|
||||
{ "word": "Relationship", "level": 3, "points": 40 },
|
||||
{ "word": "Sustainable", "level": 3, "points": 40 },
|
||||
{ "word": "Technology", "level": 3, "points": 40 },
|
||||
{ "word": "Universally", "level": 3, "points": 40 },
|
||||
{ "word": "Verification", "level": 3, "points": 40 },
|
||||
{ "word": "Workstation", "level": 3, "points": 40 },
|
||||
{ "word": "Xenophobic", "level": 3, "points": 40 },
|
||||
{ "word": "Yesterday", "level": 3, "points": 40 },
|
||||
{ "word": "Zillionaire", "level": 3, "points": 40 },
|
||||
{ "word": "Application", "level": 3, "points": 40 },
|
||||
{ "word": "Benchmarks", "level": 3, "points": 40 },
|
||||
{ "word": "Coefficient", "level": 3, "points": 40 },
|
||||
{ "word": "Declaration", "level": 3, "points": 40 },
|
||||
{ "word": "Efficiency", "level": 3, "points": 40 },
|
||||
{ "word": "Functional", "level": 3, "points": 40 },
|
||||
{ "word": "Geospatial", "level": 3, "points": 40 },
|
||||
{ "word": "Hypothesis", "level": 3, "points": 40 },
|
||||
{ "word": "Implementation", "level": 3, "points": 40 },
|
||||
{ "word": "Justification", "level": 3, "points": 40 },
|
||||
{ "word": "Keyboards", "level": 3, "points": 40 },
|
||||
{ "word": "Laboratory", "level": 3, "points": 40 },
|
||||
{ "word": "Management", "level": 3, "points": 40 },
|
||||
{ "word": "Navigation", "level": 3, "points": 40 },
|
||||
{ "word": "Orientation", "level": 3, "points": 40 },
|
||||
{ "word": "Perspective", "level": 3, "points": 40 },
|
||||
{ "word": "Qualitative", "level": 3, "points": 40 },
|
||||
{ "word": "Replication", "level": 3, "points": 40 },
|
||||
{ "word": "Simulation", "level": 3, "points": 40 },
|
||||
{ "word": "Transition", "level": 3, "points": 40 },
|
||||
{ "word": "Utilization", "level": 3, "points": 40 },
|
||||
{ "word": "Vulnerability", "level": 3, "points": 40 },
|
||||
{ "word": "Windshield", "level": 3, "points": 40 },
|
||||
{ "word": "Atmosphere", "level": 3, "points": 40 },
|
||||
{ "word": "Background", "level": 3, "points": 40 },
|
||||
{ "word": "Complexity", "level": 3, "points": 40 },
|
||||
{ "word": "Description", "level": 3, "points": 40 },
|
||||
{ "word": "Evolution", "level": 3, "points": 40 },
|
||||
{ "word": "Foundation", "level": 3, "points": 40 },
|
||||
{ "word": "Government", "level": 3, "points": 40 },
|
||||
{ "word": "Historical", "level": 3, "points": 40 },
|
||||
{ "word": "Interaction", "level": 3, "points": 40 },
|
||||
{ "word": "Leadership", "level": 3, "points": 40 },
|
||||
{ "word": "Mathematics", "level": 3, "points": 40 },
|
||||
{ "word": "Negotiation", "level": 3, "points": 40 },
|
||||
{ "word": "Observation", "level": 3, "points": 40 },
|
||||
{ "word": "Performance", "level": 3, "points": 40 },
|
||||
{ "word": "Recognition", "level": 3, "points": 40 },
|
||||
{ "word": "Statistics", "level": 3, "points": 40 },
|
||||
{ "word": "Temperature", "level": 3, "points": 40 },
|
||||
{ "word": "Understand", "level": 3, "points": 40 },
|
||||
{ "word": "Variation", "level": 3, "points": 40 },
|
||||
{ "word": "Architecture", "level": 3, "points": 40 },
|
||||
{ "word": "Biological", "level": 3, "points": 40 },
|
||||
{ "word": "Collection", "level": 3, "points": 40 },
|
||||
{ "word": "Definition", "level": 3, "points": 40 },
|
||||
{ "word": "Expression", "level": 3, "points": 40 },
|
||||
{ "word": "Generation", "level": 3, "points": 40 },
|
||||
{ "word": "Hypothesis", "level": 3, "points": 40 },
|
||||
{ "word": "Instrument", "level": 3, "points": 40 },
|
||||
{ "word": "Literature", "level": 3, "points": 40 },
|
||||
{ "word": "Measurement", "level": 3, "points": 40 },
|
||||
{ "word": "Objectives", "level": 3, "points": 40 },
|
||||
{ "word": "Parameters", "level": 3, "points": 40 },
|
||||
{ "word": "Psychology", "level": 3, "points": 40 },
|
||||
{ "word": "Resolution", "level": 3, "points": 40 },
|
||||
{ "word": "Strategies", "level": 3, "points": 40 },
|
||||
{ "word": "University", "level": 3, "points": 40 },
|
||||
{ "word": "Activities", "level": 3, "points": 40 },
|
||||
{ "word": "Comparison", "level": 3, "points": 40 },
|
||||
{ "word": "Discussion", "level": 3, "points": 40 },
|
||||
{ "word": "Experience", "level": 3, "points": 40 },
|
||||
{ "word": "Hypothesis", "level": 3, "points": 40 },
|
||||
{ "word": "Investment", "level": 3, "points": 40 },
|
||||
{ "word": "Motivation", "level": 3, "points": 40 },
|
||||
{ "word": "Operations", "level": 3, "points": 40 },
|
||||
{ "word": "Population", "level": 3, "points": 40 },
|
||||
{ "word": "Reflection", "level": 3, "points": 40 },
|
||||
{ "word": "Structures", "level": 3, "points": 40 },
|
||||
{ "word": "Vocabulary", "level": 3, "points": 40 },
|
||||
{ "word": "Appearance", "level": 3, "points": 40 },
|
||||
{ "word": "Conclusion", "level": 3, "points": 40 },
|
||||
{ "word": "Efficiency", "level": 3, "points": 40 },
|
||||
{ "word": "Expression", "level": 3, "points": 40 },
|
||||
{ "word": "Individual", "level": 3, "points": 40 }
|
||||
]
|
||||
0
module-d/.gitkeep
Normal file
BIN
module-d/analyze-image.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
module-d/generate-text.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
141
module-d/module-d-en.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 🏆 WSC2026 Test Project - Web Technologies
|
||||
## **Module D: AI-Powered Global City Explorer (Full-stack Integration)**
|
||||
|
||||
### **1. Project Overview**
|
||||
You are a full-stack developer for the 'Global City Explorer' platform. The internal AI Engineering team has developed an **'AI Middleware Server'** using local AI models (Ollama) to analyze text information and extract keywords from images. This has been provided to you as a Starter Kit.
|
||||
|
||||
Your task is to run the provided AI server, analyze its APIs, and build a sophisticated **Front-end (UI)** where administrators can create, read, update, and delete (CRUD) city information. Furthermore, you must develop a robust **Back-end (DB Storage)** that processes and securely stores the data only after it passes strict server-side validation.
|
||||
|
||||
---
|
||||
|
||||
### **2. Infrastructure Setup**
|
||||
Before beginning the task, you must ensure that your local PC meets the requirements to run the AI models and the Node.js server, and configure the environment accordingly.
|
||||
|
||||
#### **2.1. Hardware Requirements**
|
||||
To run the two AI models smoothly in a local environment, the following specifications are recommended:
|
||||
* **Memory (RAM):** Minimum 8GB / Recommended 16GB+ (Both models need to be loaded into memory).
|
||||
* **GPU:** NVIDIA GPU with 6GB+ VRAM recommended (CPU execution is possible, but image analysis processing time may be delayed).
|
||||
* **Storage:** Minimum 10GB of free space (SSD is highly recommended over HDD).
|
||||
|
||||
#### **2.2. Node.js Verification & Installation**
|
||||
A Node.js environment is required to run the back-end middleware.
|
||||
1. Open your terminal (or Command Prompt) and type `node -v` to check the installation. (A version of `v18.x.x` or higher is expected).
|
||||
2. **If not installed:** Visit the [Node.js official website (nodejs.org)](https://nodejs.org/), download the LTS (Long Term Support) version, and install it.
|
||||
|
||||
#### **2.3. Ollama (AI Engine) Verification & Installation**
|
||||
1. Open your terminal and type `ollama -v` to check the version.
|
||||
2. **If not installed:** On Windows, run **PowerShell** as an Administrator and paste the following command to install:
|
||||
`irm https://ollama.com/install.ps1 | iex`
|
||||
*(After installation, you must restart your terminal to apply the environment variables.)*
|
||||
|
||||
#### **2.4. AI Model Download & Characteristics**
|
||||
Open your terminal and run the following commands sequentially to download the two AI models used in this project. (Verify the installation using the `ollama list` command).
|
||||
|
||||
* **Download Text Processing Model:** `ollama pull phi3`
|
||||
* **[Model Characteristics]:** A Small Language Model (SLM) developed by Microsoft. It is lightweight and fast, possessing excellent text generation and logical reasoning capabilities, making it ideal for writing city introductions.
|
||||
* **Download Vision (Image) Processing Model:** `ollama pull llava`
|
||||
* **[Model Characteristics]:** A Multimodal AI equipped with 'Vision' capabilities alongside text. It analyzes uploaded landmark images to describe objects or scenery and extracts core keywords.
|
||||
|
||||
---
|
||||
|
||||
### **3. AI Middleware Server Setup & Execution**
|
||||
The provided Starter Kit contains an `ai-api` folder created by the AI engineering team. Inside, you will find `server.js` and `package.json`. **(🚨 NOTICE: Do NOT modify the code in this server under any circumstances.)**
|
||||
|
||||
1. Open a terminal and navigate to the provided `ai-api` folder directory.
|
||||
2. Enter the following command to install the required packages:
|
||||
`npm install`
|
||||
3. Once the installation is complete, start the server:
|
||||
`npm start`
|
||||
4. If the message `🚀 AI Middleware Server running on http://localhost:3000` appears in the terminal, the server is running correctly. Do not close this terminal until the competition module ends.
|
||||
|
||||
---
|
||||
|
||||
### **4. API Testing via HOPPSCOTCH**
|
||||
Before starting your development, you must use Hoppscotch (or Postman) to test the two provided APIs and understand the format of the data they return.
|
||||
|
||||
* **API 1: Text Information Generation (`POST /api/generate-text`)**
|
||||
* Body (`application/json`): `{"city_name": "Paris", "country": "France"}`
|
||||

|
||||
|
||||
* **API 2: Integrated Image Analysis (`POST /api/analyze-image`)**
|
||||
* Body (`multipart/form-data`): `city_name` (Text), `country` (Text), `image` (File type for image upload)
|
||||

|
||||
|
||||
---
|
||||
|
||||
### **5. Tasks to Complete**
|
||||
|
||||
#### **5.1. Front-end Requirements (Advanced UI/UX)**
|
||||
Build the `index.html` (or use an SPA framework) interface for administrators. The screen should be broadly divided into a **'Data Registration/Edit Area'** and a **'Registered List View Area'**.
|
||||
|
||||
* **[Data Registration & Edit Area (Form Area)]**
|
||||
* **Image Preview:** When an image is selected via `<input type="file">`, an image thumbnail must instantly appear on the screen as a preview before it is sent to the server.
|
||||
* **AI Data Integration:** Call the AI text generation API after entering the city/country name, and call the AI vision analysis API after uploading a photo. Populate the form with the returned data.
|
||||
* **Form Lock & Loading (Asynchronous State Management):** Because heavy AI analysis takes time (10~30 seconds), you must **disable** all input fields and buttons during the process to prevent duplicate clicks and user errors. Additionally, display a clear loading UI (spinner, progress bar, etc.) to optimize the User Experience.
|
||||
* **Edit Data & AI Re-fetch:**
|
||||
* Clicking an 'Edit' button on a specific city in the list should populate the form with that city's data.
|
||||
* The edit form must include a **'Re-fetch All Info from AI'** button. Clicking this button should trigger both AI APIs again based on the current city, country, and image in the form, entirely replacing the text values (description, keywords) with the newly generated AI response. The data will update in the DB only when the user clicks 'Final Update'.
|
||||
|
||||
* **[List View & Filtering Area (List & Interactive Filtering)]**
|
||||
* Fetch the globally saved city information from the DB and render it visually in a List or Card Gallery format. Each item must include an **'Edit'** and **'Delete'** button.
|
||||
* **Delete Data:** When the 'Delete' button is clicked, prevent accidental deletions by prompting the user with a browser native `confirm` dialog or a custom Modal. Upon confirmation, delete the data from the DB and immediately update the list.
|
||||
* **Hashtag Filtering:** Clicking a specific keyword (e.g., `#EiffelTower`) displayed in the list must instantly filter the entire list to show only the city cards that contain that keyword.
|
||||
|
||||
#### **5.2. Back-end Requirements (DB & Server-side Validation)**
|
||||
Construct your own back-end server and database to perfectly handle the Create, Read, Update, and Delete (CRUD) operations for your data.
|
||||
|
||||
* **DB Schema:** Design and create a `city_contents` table in MySQL/MariaDB.
|
||||
* **API Implementation (CRUD):**
|
||||
1. **GET (Read):** Returns all data saved in the DB in JSON format.
|
||||
2. **POST (Create):** `INSERT` new data into the DB and physically save the uploaded image to an upload folder.
|
||||
3. **PUT or PATCH (Update):** Updates data for a specific ID. (If a new image is submitted, you must replace the old image file or update the new file path).
|
||||
4. **DELETE (Delete):** Removes data for a specific ID from the DB.
|
||||
* **Server-side Validation (Security Check):** For POST and PUT/PATCH requests, ONLY data that passes the following validation checks should be saved to the server:
|
||||
1. **File Size Limit:** If the uploaded image exceeds **5MB**, the server must reject the request and return an error message along with HTTP status code 400 (Bad Request).
|
||||
2. **Extension Check:** The server must reject the upload if it is not an allowed image format (`.jpg`, `.jpeg`, `.png`, `.webp`).
|
||||
|
||||
#### **5.3. Additional Requirements (Pitfalls & Evaluation Points)**
|
||||
* **Data Parsing:** Within the Vision API's response, the `keywords` are not returned as a pure array, but as a **Stringified Array wrapped in JSON format**. To render these cleanly as individual tag UIs, the front-end MUST execute proper parsing (`JSON.parse`).
|
||||
* **Exception Handling:** If the back-end validation fails (e.g., File size exceeded), the front-end application must not crash. It must catch the error and clearly notify the user via an `alert` or Modal window.
|
||||
|
||||
---
|
||||
|
||||
### **6. Competitor Workspace Guide**
|
||||
|
||||
* **AI Middleware Execution Location:** The provided `ai-api` folder. (Run in Terminal 1 and leave it active).
|
||||
* **Competitor Workspace:** Your web server's root folder (e.g., `http://localhost/module-d/`).
|
||||
* **Evaluation Preparation:** At the end of the module, both the AI Middleware terminal and the web server you created must be running. Experts will connect via browser and sequentially test the following cycle:
|
||||
1. Attempt to upload an image exceeding 5MB (Check Error Handling)
|
||||
2. Normal data generation (Create)
|
||||
3. List view and Hashtag click filtering (Read & Filter)
|
||||
4. **Click 'Edit', 'Re-fetch from AI' to renew contents, and Save (Update)**
|
||||
5. **Click 'Delete' to permanently erase data (Delete)**
|
||||
|
||||
---
|
||||
|
||||
### **7. Appendix: Sample Data**
|
||||
Utilize the city/country list below for testing and final demonstration. **Recommended landmark images for testing are provided in the `sample-images` folder inside the Starter Kit.** (Prepare at least one high-resolution image larger than 5MB to test your back-end defense logic).
|
||||
|
||||
| City Name | Country Name | Landmark |
|
||||
| :--- | :--- | :--- |
|
||||
| **Paris** | France | Eiffel Tower, Louvre Museum |
|
||||
| **Seoul** | South Korea | Gyeongbokgung, N Seoul Tower |
|
||||
| **Tokyo** | Japan | Tokyo Tower, Shibuya Crossing |
|
||||
| **New York** | USA | Statue of Liberty, Times Square |
|
||||
| **Rome** | Italy | Colosseum, Trevi Fountain |
|
||||
| **Sydney** | Australia | Opera House, Harbour Bridge |
|
||||
| **Cairo** | Egypt | Pyramids of Giza, Sphinx |
|
||||
| **London** | UK | Big Ben, London Eye |
|
||||
|
||||
---
|
||||
|
||||
### **8. Mark Summary (Total: 25 Marks)**
|
||||
This table provides a summary of the points allocated per evaluation category. A detailed Marking Scheme will be provided separately.
|
||||
|
||||
| Category | Description | Max Marks |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Front-end UI & UX** | UI composition, Async state management (Loading/Locking), Data binding & Parsing | 7 |
|
||||
| **B. Data Interaction** | Data list rendering and Hashtag-based filtering functionality | 5 |
|
||||
| **C. Back-end CRUD API** | DB & File System Create, Read, Update (including AI Re-fetch), and Delete operations | 8 |
|
||||
| **D. Security & Validation**| Server-side file size/extension validation, Client-side exception handling | 5 |
|
||||
| **Total Marks** | | **25** |
|
||||
141
module-d/module-d-kr.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 🏆 WSC2026 Test Project - Web Technologies
|
||||
## **Module D: AI-Powered Global City Explorer (Full-stack Integration)**
|
||||
|
||||
### **1. 과제 개요 (Project Overview)**
|
||||
당신은 'Global City Explorer' 플랫폼의 풀스택 개발자입니다. 사내 AI 엔지니어링 팀이 로컬 AI 모델(Ollama)을 활용하여 도시의 텍스트 정보와 이미지의 키워드를 분석해 주는 **'AI 마이크로서비스(AI Middleware Server)'**를 개발하여 Starter Kit으로 제공했습니다.
|
||||
|
||||
당신의 임무는 제공된 AI 서버를 구동하고 API를 명확히 분석한 뒤, 관리자가 도시 정보를 등록, 조회, **수정 및 삭제(CRUD)** 할 수 있는 고도화된 **프론트엔드(UI)**와 엄격한 유효성 검사를 통과한 데이터만 안전하게 처리하는 **백엔드(DB Storage)**를 완벽하게 구축하는 것입니다.
|
||||
|
||||
---
|
||||
|
||||
### **2. 인프라 구축 방법 (Infrastructure Setup)**
|
||||
과제를 수행하기 전, 로컬 PC가 AI 모델과 Node.js 서버를 구동하기 위한 요구사항을 충족하는지 확인하고 환경을 세팅해야 합니다.
|
||||
|
||||
#### **2.1. 하드웨어 권장 사양 (Hardware Requirements)**
|
||||
로컬 환경에서 두 개의 AI 모델을 원활하게 구동하기 위해 아래 사양을 권장합니다.
|
||||
* **Memory (RAM):** 최소 8GB / 권장 16GB 이상 (두 모델이 메모리에 적재되어야 하므로 RAM 용량이 중요합니다.)
|
||||
* **GPU:** VRAM 6GB 이상의 NVIDIA GPU 권장 (CPU로도 구동은 가능하나, 이미지 분석 시 처리 시간이 다소 지연될 수 있습니다.)
|
||||
* **Storage:** 최소 10GB 이상의 여유 공간 (HDD보다 SSD 사용을 강력히 권장합니다.)
|
||||
|
||||
#### **2.2. Node.js 확인 및 설치**
|
||||
백엔드 미들웨어를 실행하기 위해 Node.js 환경이 필요합니다.
|
||||
1. 터미널(또는 명령 프롬프트)을 열고 `node -v`를 입력하여 설치 여부를 확인합니다. (`v18.x.x` 이상의 버전이 표출되면 정상입니다.)
|
||||
2. **미설치 시:** [Node.js 공식 홈페이지(nodejs.org)](https://nodejs.org/)에 접속하여 LTS(Long Term Support) 버전을 다운로드하고 설치하십시오.
|
||||
|
||||
#### **2.3. Ollama (AI 구동 엔진) 확인 및 설치**
|
||||
1. 터미널을 열고 `ollama -v`를 입력하여 버전 정보가 나오는지 확인합니다.
|
||||
2. **미설치 시:** Windows의 경우 **PowerShell**을 관리자 권한으로 실행한 뒤, 아래 명령어를 붙여넣어 설치를 진행하십시오.
|
||||
`irm https://ollama.com/install.ps1 | iex`
|
||||
*(설치가 완료되면 터미널을 재시작해야 환경변수가 적용됩니다.)*
|
||||
|
||||
#### **2.4. AI 모델 다운로드 및 특징**
|
||||
터미널을 열고 아래 명령어를 순차적으로 실행하여 본 과제에 사용할 2개의 AI 모델을 다운로드합니다. (`ollama list` 명령어로 설치 완료 확인)
|
||||
|
||||
* **텍스트 처리 모델 다운로드:** `ollama pull phi3`
|
||||
* **[모델 특징]:** Microsoft에서 개발한 초경량 언어 모델(SLM)입니다. 가볍고 빠르면서도 훌륭한 문장 생성 및 논리적 추론 능력을 갖추고 있어 도시의 소개글을 작성하는 데 사용됩니다.
|
||||
* **비전(이미지) 처리 모델 다운로드:** `ollama pull llava`
|
||||
* **[모델 특징]:** 텍스트뿐만 아니라 '눈(Vision)'을 가진 멀티모달(Multimodal) AI입니다. 사용자가 업로드한 명소 이미지를 분석하여 그 안의 객체나 풍경을 설명하고 핵심 키워드를 추출하는 역할을 합니다.
|
||||
|
||||
---
|
||||
|
||||
### **3. AI Middleware Server 설치 및 실행**
|
||||
제공된 Starter Kit 안에는 AI 엔지니어링 팀이 작성한 `ai-api` 폴더가 있습니다. 이 폴더 안에는 `server.js`와 `package.json`이 포함되어 있습니다. **(🚨 주의: 이 서버의 코드는 절대 수정하지 마십시오.)**
|
||||
|
||||
1. 터미널을 열고 제공된 `ai-api` 폴더 경로로 이동합니다.
|
||||
2. 아래 명령어를 입력하여 필수 패키지를 설치합니다.
|
||||
`npm install`
|
||||
3. 설치가 완료되면 서버를 실행합니다.
|
||||
`npm start`
|
||||
4. 터미널에 `🚀 AI Middleware Server running on http://localhost:3000` 메시지가 출력되면 서버가 정상 작동 중인 것입니다. 이 터미널은 과제가 끝날 때까지 닫지 마십시오.
|
||||
|
||||
---
|
||||
|
||||
### **4. HOPPSCOTCH를 활용한 API 테스트**
|
||||
본격적인 개발에 앞서, Hoppscotch(또는 Postman)를 이용하여 제공된 두 개의 API가 어떤 형태의 데이터를 반환하는지 반드시 테스트하십시오.
|
||||
|
||||
* **API 1: 텍스트 정보 생성 (`POST /api/generate-text`)**
|
||||
* Body (`application/json`): `{"city_name": "Paris", "country": "France"}`
|
||||

|
||||
|
||||
* **API 2: 이미지 통합 분석 (`POST /api/analyze-image`)**
|
||||
* Body (`multipart/form-data`): `city_name` (Text), `country` (Text), `image` (File 형식으로 이미지 업로드)
|
||||

|
||||
|
||||
---
|
||||
|
||||
### **5. 선수들이 만들어야 할 과제 (Tasks to Complete)**
|
||||
|
||||
#### **5.1. Front-end 요구사항 (Advanced UI/UX)**
|
||||
관리자가 사용할 `index.html` (또는 SPA 프레임워크) 화면을 구축하십시오. 화면은 크게 **'정보 등록/수정 영역'**과 **'등록된 목록 조회 영역'**으로 나뉘어야 합니다.
|
||||
|
||||
* **[정보 등록 및 수정 영역 (Form Area)]**
|
||||
* **이미지 미리보기 (Image Preview):** `<input type="file">`을 통해 이미지를 선택하면, 서버에 전송하기 전 화면에 즉시 이미지 썸네일이 미리보기로 표시되어야 합니다.
|
||||
* **AI 데이터 연동:** 도시명/국가명 입력 후 AI 텍스트 생성 API를 호출하고, 사진 등록 후 AI 비전 분석 API를 호출하여 반환된 데이터를 폼(Form)에 채우십시오.
|
||||
* **완벽한 비동기 상태 관리 (Form Lock & Loading):** 무거운 AI 분석 작업(10~30초 소요)이 진행되는 동안 사용자의 중복 클릭 및 오류를 방지하기 위해 **모든 입력 폼과 버튼을 비활성화(Disabled)** 처리해야 합니다. 또한 작업 진행 상태를 명확히 알 수 있는 로딩 UI(스피너 애니메이션, 프로그레스 바 등)를 화면에 표시하여 사용자 경험을 최적화하십시오.
|
||||
* **정보 수정 및 AI 다시 불러오기 (Re-fetch):**
|
||||
* 목록에서 특정 도시의 '수정' 버튼을 누르면 해당 도시의 데이터가 폼에 채워져야 합니다.
|
||||
* 수정 폼에는 **'AI에서 모든 정보 다시 불러오기'** 버튼이 있어야 합니다. 이 버튼을 클릭하면 현재 폼에 있는 도시, 국가, 이미지 데이터를 바탕으로 AI API 두 개를 다시 호출하고, 폼의 텍스트 값들(설명, 키워드 등)을 완전히 새로운 AI 응답으로 교체해야 합니다. 이후 사용자가 '최종 수정' 버튼을 누르면 업데이트됩니다.
|
||||
|
||||
* **[목록 조회 및 필터링 영역 (List & Interactive Filtering)]**
|
||||
* 최종 저장된 글로벌 도시 정보들을 DB에서 불러와 리스트(List) 또는 카드(Card) 갤러리 형태로 렌더링하십시오. 각 아이템에는 **'수정(Edit)'** 및 **'삭제(Delete)'** 버튼이 포함되어야 합니다.
|
||||
* **데이터 삭제 (Delete):** '삭제' 버튼 클릭 시, 실수로 삭제하는 것을 방지하기 위해 브라우저의 기본 `confirm` 창이나 커스텀 모달(Modal) 창으로 확인을 거친 후 DB에서 데이터를 삭제하고 목록을 즉시 갱신하십시오.
|
||||
* **해시태그 필터링:** 목록에 표시된 특정 키워드(예: `#에펠탑`)를 클릭하면, 전체 목록이 즉시 필터링되어 해당 키워드를 보유한 도시 카드들만 화면에 노출되어야 합니다.
|
||||
|
||||
#### **5.2. Back-end 요구사항 (DB 및 Server-side Validation)**
|
||||
데이터의 생성(Create), 조회(Read), 수정(Update), 삭제(Delete)를 완벽하게 처리하는 본인만의 백엔드 서버와 데이터베이스를 구축하십시오.
|
||||
|
||||
* **DB 스키마:** MySQL/MariaDB에 `city_contents` 테이블을 직접 설계하여 생성하십시오.
|
||||
* **API 구현 (CRUD):**
|
||||
1. **GET (조회):** DB에 저장된 전체 데이터를 JSON 형태로 반환합니다.
|
||||
2. **POST (생성):** 새로운 데이터를 DB에 `INSERT` 하고 이미지를 업로드 폴더에 물리적으로 저장합니다.
|
||||
3. **PUT 또는 PATCH (수정):** 특정 ID의 데이터를 수정합니다. (새로운 이미지가 전송되었다면 기존 이미지 파일을 교체하거나 새로운 경로를 업데이트해야 합니다.)
|
||||
4. **DELETE (삭제):** 특정 ID의 데이터를 DB에서 삭제합니다.
|
||||
* **저장/수정 로직의 보안 검사 (Server-side Validation):** POST 및 PUT/PATCH 요청 시, 아래의 유효성 검사를 반드시 통과한 데이터만 서버에 저장해야 합니다.
|
||||
1. **파일 용량 제한:** 업로드된 이미지 파일이 **5MB**를 초과할 경우 처리를 거부하고 HTTP 상태 코드 400(Bad Request)과 함께 에러 메시지를 반환해야 합니다.
|
||||
2. **확장자 검사:** 허용된 이미지 포맷(`.jpg`, `.jpeg`, `.png`, `.webp`)이 아닐 경우 저장을 거부해야 합니다.
|
||||
|
||||
#### **5.3. 기타 요구사항 (함정 및 평가 포인트)**
|
||||
* **데이터 파싱(Parsing):** 비전 API의 응답 중 `keywords`는 순수 배열이 아닌 **JSON 포맷으로 감싸진 텍스트 문자열(Stringified Array)**로 반환됩니다. 이를 화면에 깔끔한 태그로 렌더링하기 위해 프론트엔드에서 반드시 적절한 파싱(`JSON.parse`) 과정을 거쳐야 합니다.
|
||||
* **예외 처리:** 백엔드 유효성 검사 실패 시(예: 용량 초과) 프론트엔드 프로그램이 멈추지 않고, 캐치(Catch)하여 사용자에게 `alert` 또는 모달 창으로 명확히 안내해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
### **6. 선수 작업 안내 (Competitor Workspace Guide)**
|
||||
|
||||
* **AI Middleware 실행 위치:** 제공된 `ai-api` 폴더. (터미널 1에서 실행 후 방치)
|
||||
* **선수 작업 위치:** 본인의 웹 서버 루트 폴더(예: `http://localhost/module-d/`).
|
||||
* **평가 준비:** 과제 종료 시점에는 AI Middleware 터미널과 선수가 작성한 웹 서버가 모두 실행 중이어야 합니다. 심사위원(Expert)은 브라우저를 통해 접속하여 아래 사이클을 순차적으로 테스트할 것입니다.
|
||||
1. 5MB 초과 이미지 업로드 시도 (에러 처리 확인)
|
||||
2. 정상 데이터 생성 (Create)
|
||||
3. 목록 조회 및 해시태그 클릭 필터링 (Read & Filter)
|
||||
4. **'수정' 버튼 클릭 후 'AI 다시 불러오기'를 통한 내용 갱신 및 저장 (Update)**
|
||||
5. **'삭제' 버튼 클릭을 통한 데이터 완전 삭제 (Delete)**
|
||||
|
||||
---
|
||||
|
||||
### **7. Appendix: 테스트용 제공 데이터 (Sample Data)**
|
||||
테스트 및 최종 시연을 위해 아래의 도시/국가 목록을 활용하십시오. **과제 테스트용 명소 이미지는 제공된 Starter Kit 내의 `sample-images` 폴더에 준비되어 있습니다.** (특히 5MB가 넘는 고해상도 이미지를 하나 준비하여 백엔드 방어 로직을 테스트하십시오.)
|
||||
|
||||
| 도시명 (City) | 국가명 (Country) | 대표 명소 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Paris** | France | 에펠탑, 루브르 박물관 |
|
||||
| **Seoul** | South Korea | 경복궁, N서울타워 |
|
||||
| **Tokyo** | Japan | 도쿄 타워, 시부야 교차로 |
|
||||
| **New York** | USA | 자유의 여신상, 타임스퀘어 |
|
||||
| **Rome** | Italy | 콜로세움, 트레비 분수 |
|
||||
| **Sydney** | Australia | 오페라 하우스, 하버 브릿지 |
|
||||
| **Cairo** | Egypt | 기자 피라미드, 스핑크스 |
|
||||
| **London** | UK | 빅 벤, 런던 아이 |
|
||||
|
||||
---
|
||||
|
||||
### **8. Mark Summary (채점 요약표 - Total 25 Marks)**
|
||||
이 표는 평가 카테고리별 배점 요약입니다. 세부 채점 기준(Marking Scheme)은 별도로 제공됩니다.
|
||||
|
||||
| Category | Description | Max Marks |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Front-end UI & UX** | UI 구성, 비동기 상태 관리(로딩/잠금), 데이터 바인딩 및 파싱 처리 | 7 |
|
||||
| **B. Data Interaction** | 데이터 목록 렌더링 및 해시태그 기반 필터링 기능 | 5 |
|
||||
| **C. Back-end CRUD API** | 데이터베이스 및 파일 시스템 기반의 생성, 조회, 수정(AI 다시 불러오기 포함), 삭제 기능 | 8 |
|
||||
| **D. Security & Validation**| 서버 측 파일 용량 및 확장자 검사, 클라이언트 측 예외 처리 | 5 |
|
||||
| **Total Marks** | | **25** |
|
||||
141
module-d/module-d-uz.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 🏆 WSC2026 Test Project - Veb Texnologiyalar
|
||||
## **D Moduli: AI yordamida ishlaydigan Global City Explorer (Full-stack Integratsiya)**
|
||||
|
||||
### **1. Loyiha haqida umumiy ma'lumot (Project Overview)**
|
||||
Siz 'Global City Explorer' platformasining full-stack dasturchisisiz. Ichki AI muhandislik jamoasi matnli ma'lumotlarni tahlil qilish va rasmlardan kalit so'zlarni ajratib olish uchun mahalliy AI modellaridan (Ollama) foydalanadigan **'AI Middleware Server'** ni (AI Oraliq Serveri) ishlab chiqdi. Bu sizga Starter Kit (Boshlang'ich to'plam) sifatida taqdim etilgan.
|
||||
|
||||
Sizning vazifangiz taqdim etilgan AI serverini ishga tushirish, uning API'larini tahlil qilish va administratorlar shahar ma'lumotlarini yaratishi, o'qishi, yangilashi va o'chirishi (CRUD) mumkin bo'lgan mukammal **Front-end (UI)** yaratishdir. Bundan tashqari, ma'lumotlarni faqat qat'iy server tekshiruvidan (server-side validation) o'tgandan keyingina qayta ishlaydigan va xavfsiz saqlaydigan kuchli **Back-end (DB Storage)** ni ishlab chiqishingiz kerak.
|
||||
|
||||
---
|
||||
|
||||
### **2. Infratuzilmani sozlash (Infrastructure Setup)**
|
||||
Vazifani boshlashdan oldin, shaxsiy kompyuteringiz AI modellari va Node.js serverini ishga tushirish talablariga javob berishiga ishonch hosil qilishingiz va atrof-muhitni shunga mos ravishda sozlashingiz kerak.
|
||||
|
||||
#### **2.1. Uskuna talablari (Hardware Requirements)**
|
||||
Mahalliy muhitda ikkita AI modelini uzluksiz ishga tushirish uchun quyidagi xususiyatlar tavsiya etiladi:
|
||||
* **Xotira (RAM):** Kamida 8GB / Tavsiya etiladi 16GB+ (Ikkala model ham xotiraga yuklanishi kerak).
|
||||
* **GPU:** 6GB+ VRAM'ga ega NVIDIA GPU tavsiya etiladi (CPU orqali ishlash ham mumkin, lekin tasvirni tahlil qilish vaqti kechikishi mumkin).
|
||||
* **Disk hajmi (Storage):** Kamida 10GB bo'sh joy (HDD dan ko'ra SSD dan foydalanish qat'iy tavsiya etiladi).
|
||||
|
||||
#### **2.2. Node.js'ni tekshirish va o'rnatish**
|
||||
Back-end oraliq dasturini (middleware) ishga tushirish uchun Node.js muhiti talab qilinadi.
|
||||
1. Terminalni (yoki Command Prompt) oching va o'rnatilganligini tekshirish uchun `node -v` deb yozing. (`v18.x.x` yoki undan yuqori versiya chiqishi kutiladi).
|
||||
2. **Agar o'rnatilmagan bo'lsa:** [Node.js rasmiy veb-saytiga (nodejs.org)](https://nodejs.org/) kiring, LTS (Long Term Support) versiyasini yuklab oling va o'rnating.
|
||||
|
||||
#### **2.3. Ollama (AI Dvigateli) ni tekshirish va o'rnatish**
|
||||
1. Terminalni oching va versiyani tekshirish uchun `ollama -v` deb yozing.
|
||||
2. **Agar o'rnatilmagan bo'lsa:** Windows tizimida **PowerShell**'ni Administrator sifatida ishga tushiring va o'rnatish uchun quyidagi buyruqni kiriting:
|
||||
`irm https://ollama.com/install.ps1 | iex`
|
||||
*(O'rnatish tugagandan so'ng, muhit o'zgaruvchilari (environment variables) qo'llanilishi uchun terminalni qayta ishga tushirishingiz kerak.)*
|
||||
|
||||
#### **2.4. AI modellarini yuklab olish va ularning xususiyatlari**
|
||||
Ushbu loyihada foydalaniladigan ikkita AI modelini yuklab olish uchun terminalni oching va quyidagi buyruqlarni ketma-ket bajaring. (O'rnatish muvaffaqiyatli bo'lganini `ollama list` buyrug'i orqali tekshiring).
|
||||
|
||||
* **Matnni qayta ishlash modelini yuklab olish:** `ollama pull phi3`
|
||||
* **[Model xususiyatlari]:** Microsoft tomonidan ishlab chiqilgan Kichik Til Modeli (SLM - Small Language Model). U yengil va tez bo'lib, mukammal matn yaratish va mantiqiy fikrlash qobiliyatiga ega. Bu uni shahar haqida qisqacha ma'lumot yozish uchun ideal qiladi.
|
||||
* **Ko'rish (Tasvir) ni qayta ishlash modelini yuklab olish:** `ollama pull llava`
|
||||
* **[Model xususiyatlari]:** Matn bilan bir qatorda 'Ko'rish' (Vision) imkoniyatlari bilan jihozlangan Multimodal AI. U foydalanuvchi tomonidan yuklangan diqqatga sazovor joylar tasvirlarini tahlil qilib, ob'ektlar yoki manzaralarni tasvirlaydi va asosiy kalit so'zlarni (keywords) ajratib oladi.
|
||||
|
||||
---
|
||||
|
||||
### **3. AI Middleware Serverni o'rnatish va ishga tushirish**
|
||||
Taqdim etilgan Starter Kit ichida AI muhandislik jamoasi tomonidan yaratilgan `ai-api` papkasi mavjud. Uning ichida `server.js` va `package.json` fayllarini topasiz. **(🚨 DIQQAT: Hech qanday holatda ushbu server kodini o'zgartirmang.)**
|
||||
|
||||
1. Terminalni oching va taqdim etilgan `ai-api` papkasi katalogiga o'ting.
|
||||
2. Kerakli paketlarni o'rnatish uchun quyidagi buyruqni kiriting:
|
||||
`npm install`
|
||||
3. O'rnatish tugagach, serverni ishga tushiring:
|
||||
`npm start`
|
||||
4. Agar terminalda `🚀 AI Middleware Server running on http://localhost:3000` xabari paydo bo'lsa, server to'g'ri ishlamoqda. Musobaqa moduli tugamaguncha bu terminalni yopmang.
|
||||
|
||||
---
|
||||
|
||||
### **4. HOPPSCOTCH orqali API test qilish**
|
||||
Dasturlashni boshlashdan oldin, taqdim etilgan ikkita API qanday formatdagi ma'lumotlarni qaytarishini tushunish va test qilish uchun Hoppscotch (yoki Postman) dan foydalanishingiz SHART.
|
||||
|
||||
* **1-API: Matn ma'lumotlarini yaratish (`POST /api/generate-text`)**
|
||||
* Body (`application/json`): `{"city_name": "Paris", "country": "France"}`
|
||||

|
||||
|
||||
* **2-API: Tasvirni kompleks tahlil qilish (`POST /api/analyze-image`)**
|
||||
* Body (`multipart/form-data`): `city_name` (Text), `country` (Text), `image` (Rasm yuklash uchun File formati)
|
||||

|
||||
|
||||
---
|
||||
|
||||
### **5. Bajarilishi kerak bo'lgan vazifalar (Tasks to Complete)**
|
||||
|
||||
#### **5.1. Front-end talablari (Kengaytirilgan UI/UX)**
|
||||
Administratorlar uchun `index.html` (yoki SPA framework) interfeysini yarating. Ekran asosan **'Ma'lumotlarni ro'yxatdan o'tkazish/Tahrirlash hududi'** va **'Ro'yxatga olinganlarni ko'rish hududi'** ga bo'linishi kerak.
|
||||
|
||||
* **[Ma'lumotlarni ro'yxatdan o'tkazish va tahrirlash hududi (Form Area)]**
|
||||
* **Tasvirni oldindan ko'rish (Image Preview):** `<input type="file">` orqali rasm tanlanganda, u serverga yuborilishidan oldin ekranda rasm eskizi (thumbnail) darhol paydo bo'lishi kerak.
|
||||
* **AI ma'lumotlar integratsiyasi:** Shahar/davlat nomini kiritgandan so'ng AI matn yaratish API'sini chaqiring va rasm yuklangandan so'ng AI vizual tahlil API'sini chaqiring. Qaytarilgan ma'lumotlar bilan formani to'ldiring.
|
||||
* **Formani qulflash va yuklanish (Asynchronous State Management):** Og'ir AI tahlili biroz vaqt (10~30 soniya) talab qilganligi sababli, jarayon davomida foydalanuvchi qayta-qayta bosishining va xatoliklarning oldini olish uchun barcha kiritish maydonlari (input fields) va tugmalarni **qulflashingiz (disabled)** kerak. Bundan tashqari, foydalanuvchi tajribasini (UX) optimallashtirish uchun aniq yuklanish UI elementini (spinner, progress bar va hk.) ko'rsating.
|
||||
* **Ma'lumotlarni tahrirlash va AI'dan qayta yuklash (Re-fetch):**
|
||||
* Ro'yxatdagi ma'lum bir shahar uchun 'Tahrirlash' (Edit) tugmachasini bosish, formani o'sha shaharning ma'lumotlari bilan to'ldirishi kerak.
|
||||
* Tahrirlash formasi **'Barcha ma'lumotlarni AI'dan qayta yuklash'** tugmasini o'z ichiga olishi kerak. Ushbu tugmani bosish formadagi joriy shahar, davlat va rasmga asoslanib, ikkala AI API'sini ham qayta ishga tushirishi va matn qiymatlarini (tavsif, kalit so'zlar) butunlay yangi AI javobi bilan almashtirishi kerak. Foydalanuvchi 'Yakuniy saqlash' ni bosganidagina ma'lumotlar DB'da yangilanadi.
|
||||
|
||||
* **[Ro'yxatni ko'rish va filtrlash hududi (List & Interactive Filtering)]**
|
||||
* Butun dunyo bo'ylab saqlangan shahar ma'lumotlarini DB'dan oling va uni vizual ravishda Ro'yxat (List) yoki Kartalar Galereyasi (Card Gallery) formatida ko'rsating. Har bir elementda **'Tahrirlash' (Edit)** va **'O'chirish' (Delete)** tugmalari bo'lishi kerak.
|
||||
* **Ma'lumotlarni o'chirish (Delete):** 'O'chirish' tugmasi bosilganda, tasodifan o'chirib yuborishning oldini olish uchun brauzerning standart `confirm` oynasi yoki maxsus Modal orqali foydalanuvchidan tasdiqlashni so'rang. Tasdiqlangandan so'ng, ma'lumotlarni DB'dan o'chiring va ro'yxatni darhol yangilang.
|
||||
* **Heshteg orqali filtrlash:** Ro'yxatda ko'rsatilgan ma'lum bir kalit so'zni (masalan, `#EiffelTower`) bosish orqali darhol butun ro'yxat filtrlari ishga tushishi va faqat o'sha kalit so'zni o'z ichiga olgan shahar kartalarigina ko'rsatilishi kerak.
|
||||
|
||||
#### **5.2. Back-end talablari (DB va Server tomonida tekshirish)**
|
||||
Ma'lumotlaringizni Yaratish, O'qish, Yangilash va O'chirish (CRUD - Create, Read, Update, Delete) operatsiyalarini mukammal bajarish uchun shaxsiy back-end serveringiz va ma'lumotlar bazasini yarating.
|
||||
|
||||
* **DB Sxemasi:** MySQL/MariaDB da `city_contents` jadvalini loyihalashtiring va yarating.
|
||||
* **API ilovasi (CRUD):**
|
||||
1. **GET (O'qish):** DB'da saqlangan barcha ma'lumotlarni JSON formatida qaytaradi.
|
||||
2. **POST (Yaratish):** DB'ga yangi ma'lumotlarni `INSERT` qiladi va yuklangan rasmni jismoniy jihatdan yuklash (uploads) papkasiga saqlaydi.
|
||||
3. **PUT yoki PATCH (Yangilash):** Muayyan ID uchun ma'lumotlarni yangilaydi. (Agar yangi rasm yuborilsa, eski rasm faylini almashtirishingiz yoki yangi fayl yo'lini yangilashingiz kerak).
|
||||
4. **DELETE (O'chirish):** Muayyan ID uchun ma'lumotlarni DB'dan o'chiradi.
|
||||
* **Server tomonida tekshirish (Xavfsizlikni tekshirish):** POST va PUT/PATCH so'rovlari uchun FAQAT quyidagi tekshiruvlardan o'tgan ma'lumotlargina serverga saqlanishi kerak:
|
||||
1. **Fayl hajmi chegarasi:** Agar yuklangan rasm **5MB** dan oshsa, server so'rovni rad etishi va 400 HTTP status kodi (Bad Request) bilan birga xatolik xabarini qaytarishi kerak.
|
||||
2. **Kengaytmani tekshirish:** Agar rasm ruxsat etilgan formatda bo'lmasa (`.jpg`, `.jpeg`, `.png`, `.webp`), server yuklashni rad etishi kerak.
|
||||
|
||||
#### **5.3. Qo'shimcha talablar (Xatolar va baholash mezonlari)**
|
||||
* **Ma'lumotlarni tahlil qilish (Data Parsing):** Vision API javobida `keywords` toza massiv (array) sifatida emas, balki **JSON formatida o'ralgan matnli massiv (Stringified Array)** sifatida qaytariladi. Ularni toza va alohida teg UI sifatida ko'rsatish uchun front-end albatta to'g'ri tahlil qilish (parsing - `JSON.parse`) ni amalga oshirishi SHART.
|
||||
* **Istisnolarni ko'rib chiqish (Exception Handling):** Agar back-end tekshiruvi muvaffaqiyatsiz bo'lsa (masalan, Fayl hajmi oshib ketdi), front-end ilovasi buzilmasligi (crash) kerak. U xatoni ushlab olishi (catch) va foydalanuvchini `alert` yoki Modal oyna orqali aniq xabardor qilishi kerak.
|
||||
|
||||
---
|
||||
|
||||
### **6. Ishtirokchi ish stoli bo'yicha qo'llanma (Competitor Workspace Guide)**
|
||||
|
||||
* **AI Middleware ishga tushirish joyi:** Taqdim etilgan `ai-api` papkasi. (1-Terminalda ishga tushiring va uni ochiq qoldiring).
|
||||
* **Ishtirokchi ish stoli:** Sizning veb-serveringizning asosiy(root) papkasi (masalan, `http://localhost/module-d/`).
|
||||
* **Baholashga tayyorgarlik:** Modul oxirida AI Middleware terminali ham, siz yaratgan veb-server ham ishlab turgan bo'lishi kerak. Ekspertlar brauzer orqali ulanadi va quyidagi siklni ketma-ket sinab ko'radi:
|
||||
1. 5MB dan katta hajmdagi rasmni yuklashga urinish (Xatoni ushlab qolishni tekshirish)
|
||||
2. Oddiy ma'lumotlarni yaratish (Create)
|
||||
3. Ro'yxatni ko'rish va heshtegni bosish orqali filtrlash (Read & Filter)
|
||||
4. **'Tahrirlash' tugmasini bosish, 'AI'dan qayta yuklash' orqali tarkibni yangilash va Saqlash (Update)**
|
||||
5. **Ma'lumotni butunlay yo'q qilish uchun 'O'chirish' tugmasini bosish (Delete)**
|
||||
|
||||
---
|
||||
|
||||
### **7. Ilova: Namuna ma'lumotlar (Sample Data)**
|
||||
Test qilish va yakuniy namoyish uchun quyidagi shahar/davlat ro'yxatidan foydalaning. **Test qilish uchun tavsiya etilgan diqqatga sazovor joylarning rasmlari Starter Kit ichidagi `sample-images` papkasida taqdim etilgan.** (Back-end himoya mantiqini sinab ko'rish uchun kamida bitta 5MB dan katta yuqori aniqlikdagi rasmni tayyorlab qo'ying).
|
||||
|
||||
| Shahar nomi | Davlat nomi | Diqqatga sazovor joy (Landmark) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Paris** | France | Eyfel minorasi, Luvr muzeyi |
|
||||
| **Seoul** | South Korea | Kyonbokkun saroyi, N Seul minorasi |
|
||||
| **Tokyo** | Japan | Tokio minorasi, Shibuya chorrahasi |
|
||||
| **New York** | USA | Ozodlik haykali, Tayms maydoni |
|
||||
| **Rome** | Italy | Kolizey, Trevi favvorasi |
|
||||
| **Sydney** | Australia | Opera teatri, Harbor ko'prigi |
|
||||
| **Cairo** | Egypt | Giza piramidalari, Sfinks |
|
||||
| **London** | UK | Big Ben, London ko'zi |
|
||||
|
||||
---
|
||||
|
||||
### **8. Baholash xulosasi (Mark Summary - Jami 25 ball)**
|
||||
Ushbu jadval baholash toifalari bo'yicha ajratilgan ballar xulosasini beradi. Batafsil baholash sxemasi (Marking Scheme) alohida taqdim etiladi.
|
||||
|
||||
| Toifa (Category) | Tavsif (Description) | Maksimal Ball |
|
||||
| :--- | :--- | :---: |
|
||||
| **A. Front-end UI & UX** | UI tuzilishi, Asinxron holatni boshqarish (Yuklash/Qulflash), Ma'lumotlarni ulash (Binding) va Tahlil qilish (Parsing) | 7 |
|
||||
| **B. Data Interaction** | Ma'lumotlar ro'yxatini ko'rsatish (rendering) va Heshtegga asoslangan filtrlash funksiyasi | 5 |
|
||||
| **C. Back-end CRUD API** | DB va Fayl tizimida Yaratish, O'qish, Yangilash (AI'dan qayta yuklash bilan birga) va O'chirish amallari | 8 |
|
||||
| **D. Security & Validation**| Server tomonida fayl hajmi/kengaytmani tekshirish, Mijoz tomonida istisnolarni (xatolarni) ko'rib chiqish | 5 |
|
||||
| **Jami Ballar** | | **25** |
|
||||
1017
module-d/package-lock.json
generated
Normal file
18
module-d/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "wsc2026-module-b-ai-middleware",
|
||||
"version": "1.0.0",
|
||||
"description": "WSC2026 Web Technologies - Test Project: AI Middleware",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "WorldSkills Expert Team",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
},
|
||||
"keywords": [],
|
||||
"type": "commonjs"
|
||||
}
|
||||
BIN
module-d/sample_images/Cairo_Egypt_1.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
module-d/sample_images/Cairo_Egypt_2.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
module-d/sample_images/London_UK_1.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
module-d/sample_images/London_UK_2.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
module-d/sample_images/New York_USA_1.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
module-d/sample_images/New York_USA_2.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
module-d/sample_images/Paris_France_1.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
module-d/sample_images/Paris_france_2.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
module-d/sample_images/Rome_Italy_1.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
module-d/sample_images/Rome_Italy_2.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
module-d/sample_images/Seoul_Korea_1.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
module-d/sample_images/Seoul_Korea_2.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
module-d/sample_images/Sydney_Australia_1.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
module-d/sample_images/Sydney_Australia_2.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
module-d/sample_images/Tokyo_Japan_1.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
module-d/sample_images/Tokyo_Japan_2.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
module-d/sample_images/over_5mb/OrexModel.115728.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
173
module-d/server.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* WSC2026 Web Technologies - Test Project: AI Middleware Server
|
||||
* ============================================================================
|
||||
* 🚨 [Competitor Notice]
|
||||
* You do NOT need to modify this file. This is a black-box AI microservice.
|
||||
* Your task is to run this server, test the APIs, and build your own
|
||||
* Front-end and Back-end (Database) application that communicates with it.
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const multer = require('multer');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Use memory storage (Converts image buffer directly to Base64)
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
const OLLAMA_API = 'http://localhost:11434/api/generate';
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// API 1: Text AI (Generate City Description and Country Info)
|
||||
// ----------------------------------------------------------------------------
|
||||
app.post('/api/generate-text', async (req, res) => {
|
||||
const { city_name, country } = req.body;
|
||||
if (!city_name || !country) {
|
||||
return res.status(400).json({ error: 'city_name and country are required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(OLLAMA_API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'phi3',
|
||||
// 🚀 Advanced Prompt: Request city description and country info in JSON format
|
||||
prompt: `Provide information about ${city_name} in ${country}.
|
||||
Return a JSON object with exactly two keys:
|
||||
1. "city_description": A 2-sentence tourist introduction for the city.
|
||||
2. "country_info": One interesting or essential fact about the country ${country}.
|
||||
DO NOT output any explanations. JUST the JSON object.`,
|
||||
format: 'json',
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
const rawText = data.response;
|
||||
|
||||
console.log(`[Text API] AI Original Response for ${city_name}, ${country}:`, rawText);
|
||||
|
||||
// --- 🛡️ Text Hallucination Defense Logic ---
|
||||
let cityDescription = "Failed to generate city description. Please enter manually.";
|
||||
let countryInfo = "Failed to generate country information. Please enter manually.";
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawText);
|
||||
cityDescription = parsed.city_description || cityDescription;
|
||||
countryInfo = parsed.country_info || countryInfo;
|
||||
} catch (e) {
|
||||
// Fallback: Use regular expressions if JSON parsing fails
|
||||
console.warn("[Text API] JSON parsing failed. Attempting regex extraction.");
|
||||
const descMatch = rawText.match(/"city_description"\s*:\s*"([^"]+)"/i);
|
||||
if (descMatch) cityDescription = descMatch[1];
|
||||
const infoMatch = rawText.match(/"country_info"\s*:\s*"([^"]+)"/i);
|
||||
if (infoMatch) countryInfo = infoMatch[1];
|
||||
}
|
||||
|
||||
res.json({
|
||||
city_description: cityDescription,
|
||||
country_info: countryInfo
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Text API] Error:", error);
|
||||
res.status(500).json({ error: 'Text AI processing failed. Ensure Ollama is running.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// API 2: Vision AI (Integrated Image Analysis - Description and Keywords)
|
||||
// ----------------------------------------------------------------------------
|
||||
app.post('/api/analyze-image', upload.single('image'), async (req, res) => {
|
||||
const file = req.file;
|
||||
const { city_name, country } = req.body;
|
||||
|
||||
if (!file || !city_name || !country) {
|
||||
return res.status(400).json({ error: 'image, city_name, and country are required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Image = file.buffer.toString('base64');
|
||||
const response = await fetch(OLLAMA_API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'llava',
|
||||
prompt: `Analyze this image of ${city_name}, ${country}.
|
||||
Return a JSON object with:
|
||||
1. "description": A short 1-2 sentence description of the image.
|
||||
2. "keywords": An array of exactly 3 descriptive words.
|
||||
JUST the JSON object. DO NOT output any markdown, tags, or extra text.`,
|
||||
images: [base64Image],
|
||||
format: 'json',
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const rawAiText = data.response;
|
||||
|
||||
console.log(`[Vision API] AI Original Response for ${city_name}, ${country}:`, rawAiText);
|
||||
|
||||
// --- 🛡️ Hallucination Defense & Data Sanitization Logic ---
|
||||
let finalDescription = "No description generated. Please enter manually.";
|
||||
let finalKeywords = [];
|
||||
|
||||
try {
|
||||
// Attempt 1: Force extract inside curly braces {} in case AI adds markdown wrappers
|
||||
const jsonMatch = rawAiText.match(/\{[\s\S]*\}/);
|
||||
const targetText = jsonMatch ? jsonMatch[0] : rawAiText;
|
||||
const parsed = JSON.parse(targetText);
|
||||
|
||||
if (parsed.description) finalDescription = parsed.description;
|
||||
if (parsed.keywords) {
|
||||
if (Array.isArray(parsed.keywords)) {
|
||||
finalKeywords = parsed.keywords;
|
||||
} else if (typeof parsed.keywords === 'string') {
|
||||
// Split into an array if keywords arrive as a comma-separated string
|
||||
finalKeywords = parsed.keywords.split(',');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[Vision API] JSON parsing failed. Attempting regex fallback.");
|
||||
|
||||
// Attempt fallback extraction for description
|
||||
const descMatch = rawAiText.match(/"description"\s*:\s*"([^"]+)"/i);
|
||||
if (descMatch) finalDescription = descMatch[1];
|
||||
|
||||
// Attempt fallback extraction for keywords array
|
||||
const arrMatch = rawAiText.match(/\[(.*?)\]/s);
|
||||
if (arrMatch) finalKeywords = arrMatch[1].replace(/["']/g, '').split(',');
|
||||
}
|
||||
|
||||
// Sanitize the keywords array (trim, remove brackets/quotes, filter empty, limit to 3)
|
||||
finalKeywords = finalKeywords
|
||||
.map(w => w.trim().replace(/[{}"\[\]]/g, ''))
|
||||
.filter(w => w.length > 0)
|
||||
.slice(0, 3);
|
||||
|
||||
if (finalKeywords.length === 0) {
|
||||
finalKeywords = ["analysis_failed", "manual_input_required", "error"];
|
||||
}
|
||||
|
||||
// Finally, return the description (Text) and keywords (Stringified Array)
|
||||
res.json({
|
||||
description: finalDescription,
|
||||
keywords: JSON.stringify(finalKeywords)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Vision API] Error:", error);
|
||||
res.status(500).json({ error: 'Vision AI processing failed. Ensure Ollama is running.' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 AI Middleware Server running on http://localhost:${PORT}`);
|
||||
console.log(`- Text API: POST http://localhost:${PORT}/api/generate-text`);
|
||||
console.log(`- Vision API: POST http://localhost:${PORT}/api/analyze-image`);
|
||||
});
|
||||