# Design — todo-app (간단한 To-Do 앱) > PDCA Phase: **Design** · Feature: `todo-app` · 작성일: 2026-06-15 > 선택 아키텍처: **Option C — 실용 균형** · 첨부 다운로드: **서버 프록시 스트리밍** > 상위 문서: [Plan](../../01-plan/features/todo-app.plan.md) ## Context Anchor | 항목 | 내용 | |------|------| | **WHY** | 사내 워크스페이스 샘플을 실제로 쓸 수 있는 최소 To-Do 앱으로 만들어 DB+S3 활용 패턴을 보여준다. | | **WHO** | 로그인 없이 접근하는 단일 사용자/소규모 팀(단일 공용 목록). | | **RISK** | 첨부파일 처리(크기/타입), schema 마이그레이션 누락, S3 키-DB 레코드 정합성. | | **SUCCESS** | 브라우저에서 할 일 CRUD + 첨부 업로드/다운로드가 동작하고 `npm run db:check`/`minio:check` 통과. | | **SCOPE** | IN: todo CRUD, 첨부 1개/항목, 웹 UI, REST API. OUT: 인증/멀티유저, 마감일/태그/검색, 실시간. | --- ## 1. Overview 기존 Express 앱(`src/server.js`)에 To-Do 도메인을 추가한다. 구조화 데이터는 Postgres(`src/db.js` pool), 첨부파일 바이너리는 MinIO/S3(`src/s3.js` client)에 저장하고 두 저장소를 S3 object key로 연결한다. **아키텍처 결정 (Option C — 실용 균형)**: route / repo / attachments 3개 모듈로 관심사를 분리하되, 별도 service 계층은 두지 않는다(라우트 핸들러가 repo+attachments를 조합). "간단한 앱" 목표에 맞는 최소한의 경계. **다운로드 결정 (서버 프록시 스트리밍)**: 브라우저는 앱 엔드포인트에만 요청하고, 서버가 `GetObjectCommand`로 S3 객체를 받아 응답 스트림으로 전달. 내부 MinIO 호스트(`minio.bokdev.in`) 노출 없이 환경 무관하게 동작. → presigned URL 불필요, `@aws-sdk/s3-request-presigner` 의존성 추가하지 않음. ## 2. 모듈 구조 ``` src/ config.js (기존) 변경 없음 db.js (기존) pool, pingDb — 재사용 s3.js (기존) s3 client, pingS3 — 재사용. GetObject/PutObject/DeleteObject 추가 export schema.js (신규) todo 테이블 멱등 생성 (앱 시작 시 ensureSchema 호출) todos.repo.js (신규) DB CRUD 쿼리 (pool 사용) attachments.js (신규) S3 put / get(stream) / delete 래퍼 routes.todos.js (신규) Express Router — REST 핸들러 server.js (수정) express.json/static 미들웨어, schema 부트스트랩, 라우터 마운트 public/ index.html (신규) 단일 페이지 웹 UI (vanilla JS fetch) ``` ## 3. 데이터 모델 ```sql -- schema.js 에서 CREATE TABLE IF NOT EXISTS 로 멱등 실행 CREATE TABLE IF NOT EXISTS todo ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), title text NOT NULL, done boolean NOT NULL DEFAULT false, attachment_key text, -- S3 object key (없으면 NULL) attachment_name text, -- 원본 파일명 attachment_type text, -- content-type created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_todo_created_at ON todo (created_at DESC); ``` - `gen_random_uuid()`는 pgcrypto 확장이 필요. 미존재 환경 대비: schema.js에서 `CREATE EXTENSION IF NOT EXISTS pgcrypto`를 먼저 시도하고, 권한 부족으로 실패하면 앱에서 `crypto.randomUUID()`로 id를 생성해 INSERT (fallback). - **S3 키 규칙**: `todos/{todo_id}/{원본파일명}` — 버킷은 `config.s3.bucket`(`coolify-user-data`). ## 4. API 계약 | Method | Path | Request | Response (성공) | 비고 | |--------|------|---------|------|------| | GET | `/` | — | `text/html` (index.html) | 정적 제공 | | GET | `/api/todos` | — | `200 { data: Todo[] }` | created_at DESC | | POST | `/api/todos` | `multipart/form-data`: `title`(필수), `file`(선택) | `201 { data: Todo }` | file 있으면 S3 업로드 후 key 기록 | | PATCH | `/api/todos/:id` | `application/json`: `{ done: boolean }` | `200 { data: Todo }` | 토글 | | DELETE | `/api/todos/:id` | — | `204` | S3 첨부 먼저 삭제 후 DB row 삭제 | | GET | `/api/todos/:id/attachment` | — | `200` 스트림(`Content-Type`, `Content-Disposition`) | 프록시 스트리밍, 첨부 없으면 404 | **공통 규약** - 성공 바디: `{ data: ... }`, 에러 바디: `{ error: string }`. - 입력 검증: `title` 비어있으면 `400 { error }`. 잘못된 `:id`(uuid 형식 아님) → `400`. 없는 todo → `404`. - 업로드 제한: multer `limits.fileSize = 10MB`. 초과 시 `413 { error }`. ### Todo 객체 형태 ```json { "id": "uuid", "title": "string", "done": false, "attachment_name": "report.pdf | null", "attachment_type": "application/pdf | null", "hasAttachment": true, "created_at": "ISO8601" } ``` > `attachment_key`는 응답에서 제외(내부 키 비노출), 대신 `hasAttachment` boolean 노출. ## 5. 처리 흐름 (핵심 시퀀스) **POST /api/todos (첨부 포함)** 1. multer가 `title` + `file`(메모리 버퍼) 파싱 → title 검증 2. id 생성(또는 INSERT 후 반환된 id) → S3 key `todos/{id}/{filename}` 결정 3. `attachments.put(key, buffer, contentType)` → S3 업로드 4. repo: INSERT (attachment_key/name/type 포함) → 201 반환 5. 3단계 실패 시 DB INSERT 하지 않음(또는 보상 삭제) → 500 **DELETE /api/todos/:id** 1. repo: SELECT attachment_key 2. key 있으면 `attachments.del(key)` 먼저 시도(실패는 로깅하되 계속) 3. repo: DELETE row → 204 **GET /api/todos/:id/attachment** 1. repo: SELECT attachment_key/name/type → 없으면 404 2. `attachments.getStream(key)` → `Content-Type`, `Content-Disposition: attachment; filename=...` 세팅 3. S3 Body 스트림을 res로 pipe ## 6. 에러 처리 & 정합성 | 상황 | 처리 | |------|------| | S3 업로드 성공, DB INSERT 실패 | 업로드한 객체 보상 삭제(best-effort) 후 500 | | DB 삭제 전 S3 삭제 실패 | 경고 로깅 후 DB 삭제 진행(고아 객체는 허용, 데이터 무결성 우선) | | pgcrypto 미존재 | `crypto.randomUUID()` fallback | | 파일 크기 초과 | multer 에러 → 413 | ## 7. 의존성 변경 | 패키지 | 용도 | 비고 | |--------|------|------| | `multer` | multipart 업로드 파싱(메모리 스토리지) | 신규 추가 | | `@aws-sdk/client-s3` | `GetObjectCommand`/`PutObjectCommand`/`DeleteObjectCommand` | 기존, 명령만 추가 사용 | > presigned 방식 미채택 → `@aws-sdk/s3-request-presigner` 추가하지 않음 (Plan 대비 변경점). > 추가 후 `npm install`로 lock 갱신. ## 8. Test Plan **L1 — API 엔드포인트 (서버 기동 시)** - `GET /api/todos` → 200, `{ data: [] }` 형태 - `POST /api/todos` (title만) → 201, data.id 존재 - `POST /api/todos` (title 누락) → 400 - `PATCH /api/todos/:id {done:true}` → 200, data.done=true - `DELETE /api/todos/:id` → 204 - `GET /api/todos/:bad-uuid/attachment` → 400/404 **L2 — UI 액션 (수동 또는 Playwright)** - 페이지 로드 → 목록 렌더 - 입력+추가 → 목록에 새 항목, 입력창 비워짐 - 체크박스 토글 → done 반영 - 삭제 → 항목 사라짐 - 파일 첨부 추가 → 다운로드 링크 노출, 클릭 시 파일 받아짐 **L3 — E2E 시나리오** - 첨부 포함 추가 → 다운로드 → 삭제까지 전체 흐름 무오류 - 연결 점검: `npm run db:check`, `npm run minio:check` 통과 (SC-05) - 컨테이너: `podman build` + `podman run --env-file .project-env` 동작 (SC-06) ## 9. Success Criteria 매핑 | SC | Design 반영 위치 | |----|------------------| | SC-01 (CRUD UI) | §2 routes.todos.js + public/index.html, §4 API | | SC-02 (업로드→S3+DB key) | §5 POST 흐름, §3 데이터 모델 | | SC-03 (다운로드) | §5 GET attachment, 프록시 스트리밍 | | SC-04 (삭제 시 S3도 삭제) | §5 DELETE 흐름, §6 정합성 | | SC-05 (db/minio check) | §8 L3 | | SC-06 (podman) | §8 L3 | ## 10. 보안/규약 체크 - 비밀값: 전부 env(`config.js`) 경유, 코드/커밋 금지 (CLAUDE.md 준수). - 외부 연결: db/s3/config 모듈만 경유. - 입력 검증: title 길이/공백, uuid 형식, 파일 크기·(선택)타입. - 내부 S3 키/호스트 비노출(프록시 + 응답에서 key 제외). ## 11. Implementation Guide ### 11.1 구현 순서 1. `src/schema.js` — ensureSchema (pgcrypto 시도 + CREATE TABLE) 2. `src/s3.js` 확장 — putObject/getObjectStream/deleteObject export 3. `src/attachments.js` — 키 생성 규칙 + put/getStream/del 4. `src/todos.repo.js` — list/create/setDone/getById/remove 5. `src/routes.todos.js` — 6개 핸들러 + multer 미들웨어 + 검증 6. `src/server.js` — express.json, static(public), ensureSchema 호출, 라우터 마운트 7. `public/index.html` — fetch 기반 UI 8. `package.json` — multer 추가, `npm install` ### 11.2 핵심 파일 - `src/routes.todos.js` (가장 로직 밀집), `src/attachments.js`, `public/index.html` ### 11.3 Session Guide (Module Map) | 모듈 키 | 범위 | 포함 파일 | 의존 | |---------|------|-----------|------| | `module-1` (데이터 계층) | schema + repo + s3/attachments | schema.js, s3.js(확장), attachments.js, todos.repo.js | 없음 | | `module-2` (API 계층) | 라우트 + 서버 연결 + 의존성 | routes.todos.js, server.js, package.json | module-1 | | `module-3` (UI) | 웹 프런트 | public/index.html | module-2 | **권장 세션 분할** - 1세션에 전부 가능(소규모). 분할 시: `--scope module-1` → `--scope module-2` → `--scope module-3`. ## 12. 다음 단계 `/pdca do todo-app` (전체) 또는 `/pdca do todo-app --scope module-1` (점진 구현).