Skip to content

Commit 76fff79

Browse files
committed
docs: add architecture overview
1 parent 645d88a commit 76fff79

2 files changed

Lines changed: 317 additions & 0 deletions

File tree

docs/wiki/Architecture-Overview.md

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
# Architecture Overview
2+
3+
> 뮤런의 시스템 구성·코드 레이아웃·요청 흐름을 한 페이지에 담은 발표용 정리.
4+
> 세부 결정은 [`02-Tech-Stack`](02-Tech-Stack), 화면/권한 매트릭스는 [`03-Screen-Flow`](03-Screen-Flow) 참조.
5+
6+
## 1. 시스템 아키텍처
7+
8+
```mermaid
9+
flowchart LR
10+
User(("👤 부원<br/>모바일·데스크탑"))
11+
Google{{"🔐 Google OAuth<br/>hd: snu.ac.kr"}}
12+
13+
subgraph GH["🐙 GitHub"]
14+
direction TB
15+
Repo["repo<br/>main · dev"]
16+
Actions["⚙️ Actions<br/>CI · Deploy"]
17+
GHCR[("📦 GHCR<br/>container registry")]
18+
end
19+
20+
subgraph N100["🖥️ 자체 서버 N100 · Ubuntu"]
21+
direction TB
22+
Caddy["🛡️ Caddy<br/>HTTPS · 80/443"]
23+
App["▲ Next.js 15<br/>Server Actions · Auth.js v5"]
24+
DBVol[("🗄️ SQLite<br/>murun-prod_data")]
25+
UpVol[("🖼️ uploads<br/>murun-prod_uploads")]
26+
end
27+
28+
User ==>|"HTTPS<br/>murun.duckdns.org"| Caddy
29+
Caddy ==> App
30+
App ==> DBVol
31+
App ==> UpVol
32+
App <-->|"OIDC callback"| Google
33+
34+
Repo -.->|push| Actions
35+
Actions -.->|"build · push image"| GHCR
36+
Actions -.->|"SSH (main only)<br/>compose pull · up -d"| App
37+
GHCR -.->|image| App
38+
39+
classDef user fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0f172a
40+
classDef ext fill:#fef3c7,stroke:#b45309,stroke-width:2px,color:#0f172a
41+
classDef gh fill:#ede9fe,stroke:#6d28d9,stroke-width:2px,color:#0f172a
42+
classDef srv fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#0f172a
43+
classDef data fill:#fde68a,stroke:#92400e,stroke-width:2px,color:#0f172a
44+
45+
class User user
46+
class Google ext
47+
class Repo,Actions,GHCR gh
48+
class Caddy,App srv
49+
class DBVol,UpVol data
50+
51+
style GH fill:#f5f3ff,stroke:#7c3aed,stroke-width:1px
52+
style N100 fill:#f0fdf4,stroke:#16a34a,stroke-width:1px
53+
```
54+
55+
읽는 법:
56+
57+
- **굵은 실선** = 런타임 사용자 트래픽
58+
- **점선** = GitHub Actions 빌드/배포 파이프라인
59+
- N100 한 호스트 안에 Caddy / Next.js / SQLite / uploads 가 같이 산다 (외부 DB 없음).
60+
- 인증만 외부(Google OAuth)에 위탁, 그 외 사용자 데이터는 모두 자체 볼륨.
61+
- `dev` push는 GHCR `staging-<sha>` 이미지 빌드/푸시까지만 가고 N100엔 들어가지 않는다. `main` push만 prod 배포.
62+
63+
## 2. 데이터 모델 (Prisma)
64+
65+
```mermaid
66+
erDiagram
67+
USER ||--o{ ACCOUNT : "has"
68+
USER ||--o{ SESSION : "hosts"
69+
USER ||--o{ PARTICIPATION : "writes"
70+
SESSION ||--o{ PARTICIPATION : "has"
71+
USER }o..o| USER : "approved by"
72+
73+
USER {
74+
string id PK
75+
string email UK
76+
string name
77+
boolean approved
78+
string role "MEMBER | ADMIN"
79+
datetime joinedAt
80+
}
81+
ACCOUNT {
82+
string id PK
83+
string userId FK
84+
string provider "google"
85+
string providerAccountId
86+
}
87+
SESSION {
88+
int id PK
89+
datetime date
90+
string location
91+
string weather
92+
string groupPhotoPath
93+
string hostId FK
94+
}
95+
PARTICIPATION {
96+
string id PK
97+
int sessionId FK
98+
string userId FK
99+
float distanceKm
100+
int durationSec
101+
string note
102+
}
103+
```
104+
105+
핵심:
106+
107+
- `Session.id``Int autoincrement` — 내부 동아리용이라 enumeration 차단보다 단톡방 공유 가독성 우선.
108+
- `(sessionId, userId)` unique — 본인은 한 세션에 행 1개.
109+
- `User.approved``User.role` 분리 — 가입 정책(화이트리스트)과 운영 권한은 다른 축.
110+
- 페이스(`pace`)는 저장 안 함 — `distanceKm + durationSec` 둘 다 있을 때 derived. (`lib/pace.ts`)
111+
- 사진은 파일 경로(`groupPhotoPath`)만 저장 — 원본은 `uploads/` 볼륨.
112+
113+
스키마 원본: [`prisma/schema.prisma`](https://github.com/boostcampwm-snu-2026-1/murun-peterabcd/blob/main/prisma/schema.prisma).
114+
권한 매트릭스: [`03-Screen-Flow §5`](03-Screen-Flow#5-권한-매트릭스).
115+
116+
## 3. 모듈 레이어
117+
118+
```mermaid
119+
flowchart TB
120+
subgraph UI["🖥️ UI 계층 (app/)"]
121+
direction LR
122+
Pages["routes<br/>/login · /sessions · /me<br/>/runners · /admin/members"]
123+
OG["OG · PWA<br/>opengraph-image · manifest · icon"]
124+
UIComp["components/<br/>ui · form (shadcn)"]
125+
end
126+
127+
subgraph ACTIONS["⚡ 서버 액션 (app/**/actions.ts)"]
128+
Acts["createSession · upsertParticipation<br/>uploadPhoto · approveMember<br/>updateSession · deleteSession"]
129+
end
130+
131+
subgraph PURE["🧮 순수 로직 (lib/)"]
132+
direction LR
133+
FormLib["participation-form<br/>session-form<br/>session-filters"]
134+
Pace["pace · members"]
135+
Limits["upload-limits"]
136+
end
137+
138+
subgraph SRVLIB["🔒 서버 전용 (lib/, server-only)"]
139+
direction LR
140+
DBClient["db (Prisma)"]
141+
AuthLib["auth · auth.config · guard"]
142+
UpLib["uploads · upload-url"]
143+
SessQ["sessions · members 조회"]
144+
end
145+
146+
subgraph DATA["💾 저장소"]
147+
direction LR
148+
Prisma[("prisma/<br/>schema.prisma + migrations")]
149+
Files[("uploads/ 볼륨")]
150+
end
151+
152+
subgraph TESTS["🧪 테스트"]
153+
direction LR
154+
Unit["test/ (Vitest + RTL)"]
155+
E2E["e2e/ (Playwright)"]
156+
end
157+
158+
subgraph OPS["🚀 배포 · 운영"]
159+
direction LR
160+
Deploy["deploy/<br/>Dockerfile · compose · Caddyfile"]
161+
CI[".github/workflows/<br/>ci.yml · deploy.yml"]
162+
Scripts["scripts/<br/>cleanup-orphan-uploads"]
163+
end
164+
165+
Pages --> Acts
166+
Pages --> UIComp
167+
Pages --> FormLib
168+
Pages --> Pace
169+
Acts --> FormLib
170+
Acts --> Limits
171+
Acts --> AuthLib
172+
Acts --> UpLib
173+
Acts --> DBClient
174+
SessQ --> DBClient
175+
DBClient --> Prisma
176+
UpLib --> Files
177+
178+
FormLib -.->|단위 테스트| Unit
179+
Limits -.->|단위 테스트| Unit
180+
UIComp -.->|컴포넌트 테스트| Unit
181+
Pages -.->|smoke| E2E
182+
183+
CI -.->|컨테이너 빌드| Deploy
184+
185+
classDef ui fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0f172a
186+
classDef act fill:#fde68a,stroke:#b45309,stroke-width:2px,color:#0f172a
187+
classDef dom fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#0f172a
188+
classDef srv fill:#fecaca,stroke:#b91c1c,stroke-width:2px,color:#0f172a
189+
classDef data fill:#e9d5ff,stroke:#7e22ce,stroke-width:2px,color:#0f172a
190+
classDef tst fill:#cffafe,stroke:#0e7490,stroke-width:2px,color:#0f172a
191+
classDef ops fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#0f172a
192+
193+
class Pages,OG,UIComp ui
194+
class Acts act
195+
class FormLib,Pace,Limits dom
196+
class DBClient,AuthLib,UpLib,SessQ srv
197+
class Prisma,Files data
198+
class Unit,E2E tst
199+
class Deploy,CI,Scripts ops
200+
201+
style UI fill:#eff6ff,stroke:#1d4ed8,stroke-width:1px
202+
style ACTIONS fill:#fffbeb,stroke:#b45309,stroke-width:1px
203+
style PURE fill:#f0fdf4,stroke:#15803d,stroke-width:1px
204+
style SRVLIB fill:#fef2f2,stroke:#b91c1c,stroke-width:1px
205+
style DATA fill:#faf5ff,stroke:#7e22ce,stroke-width:1px
206+
style TESTS fill:#ecfeff,stroke:#0e7490,stroke-width:1px
207+
style OPS fill:#f8fafc,stroke:#475569,stroke-width:1px
208+
```
209+
210+
읽는 법:
211+
212+
- **위에서 아래로** 의존 방향. 위 계층은 아래 계층을 부르고, 그 반대는 없다.
213+
- `lib/` 는 의도적으로 두 덩어리. **순수 로직**은 단위 테스트가 직접 붙고, **서버 전용**`import "server-only"` 가드로 클라이언트 번들에 새지 않게 한다.
214+
- 서버 액션(`app/**/actions.ts`)이 도메인 진입점. 라우트는 항상 액션을 거쳐 DB에 닿는다.
215+
- 테스트는 “가장 깨지기 쉬운 경계”에 우선 박는다 — 폼 파싱, 필터 파싱, 업로드 제한, 핵심 사용자 흐름 smoke.
216+
217+
## 4. 폴더 구조 (실제, Week 3 종료 시점)
218+
219+
```
220+
murun-peterabcd/
221+
├─ app/
222+
│ ├─ layout.tsx · page.tsx · globals.css
223+
│ ├─ login/page.tsx # Auth.js v5 + 에러 한국어
224+
│ ├─ pending/page.tsx # 가입 승인 대기
225+
│ ├─ admin/members/page.tsx # 승인/거절 (requireAdmin)
226+
│ ├─ sessions/
227+
│ │ ├─ page.tsx # 아카이브 + 필터
228+
│ │ ├─ _components/{FilterBar,SessionCard,EmptyState}.tsx
229+
│ │ ├─ new/{page.tsx,actions.ts,_components/}
230+
│ │ └─ [id]/
231+
│ │ ├─ page.tsx # 세션 상세
232+
│ │ ├─ opengraph-image.tsx # 카톡 OG (sharp)
233+
│ │ ├─ actions.ts # upsertParticipation
234+
│ │ ├─ photo-actions.ts # 사진 업로드/교체/삭제
235+
│ │ ├─ edit/{page.tsx,actions.ts,_components/EditSessionForm.tsx}
236+
│ │ └─ _components/{PhotoSection,PhotoReplaceButton,MyParticipationForm,...}.tsx
237+
│ ├─ me/page.tsx # 내 통계
238+
│ ├─ runners/[id]/page.tsx # 멤버 통계
239+
│ ├─ api/
240+
│ │ ├─ auth/[...nextauth]/route.ts # Auth.js handler
241+
│ │ ├─ health/route.ts # /api/health
242+
│ │ └─ uploads/[...path]/route.ts # 인증된 정적 파일 서빙
243+
│ ├─ opengraph-image.tsx # 홈 OG
244+
│ └─ icon.tsx · apple-icon.tsx · icons/[size]/route.tsx · manifest.ts # PWA
245+
├─ components/
246+
│ ├─ ui/{button,input,label,textarea}.tsx # shadcn 복사본
247+
│ └─ form/{SubmitButton,ErrorAlert}.tsx # 공용 폼 조각
248+
├─ lib/
249+
│ ├─ db.ts # PrismaClient (싱글톤)
250+
│ ├─ auth.ts · auth.config.ts # Auth.js v5 (Google + adapter)
251+
│ ├─ guard.ts # requireUser · requireAdmin · requireHostOrAdmin · requireE2eUser
252+
│ ├─ sessions.ts # 아카이브 조회 + raw SQL count 필터
253+
│ ├─ members.ts # 누적 거리 · 평균 페이스 · 추이
254+
│ ├─ pace.ts # 페이스 순수 계산
255+
│ ├─ participation-form.ts # 본인 행 파싱 (테스트 대상)
256+
│ ├─ session-form.ts # 세션 폼 파싱
257+
│ ├─ session-filters.ts # URL 쿼리 ↔ 필터 객체
258+
│ ├─ uploads.ts # 디스크 IO (server-only)
259+
│ ├─ upload-url.ts # 공개 URL helper
260+
│ ├─ upload-limits.ts # 15MB / MIME / checkUploadFile (공유)
261+
│ ├─ og-font.ts # OG 폰트 로딩
262+
│ └─ utils.ts
263+
├─ prisma/
264+
│ ├─ schema.prisma # User · Account · Session · Participation
265+
│ └─ migrations/
266+
├─ deploy/
267+
│ ├─ Dockerfile # multi-stage standalone
268+
│ ├─ docker-compose.yml # caddy + app
269+
│ ├─ Caddyfile # 자동 HTTPS
270+
│ ├─ entrypoint.sh # migrate deploy + node server.js
271+
│ ├─ .env.example · README.md
272+
├─ scripts/
273+
│ └─ cleanup-orphan-uploads.mjs # DB ↔ 파일 비교 후 삭제
274+
├─ .github/
275+
│ ├─ workflows/{ci.yml,deploy.yml}
276+
│ ├─ ISSUE_TEMPLATE/ · PULL_REQUEST_TEMPLATE.md
277+
├─ test/ # Vitest 단위/컴포넌트
278+
├─ e2e/ # Playwright smoke
279+
├─ docs/wiki/ # 이 문서 포함
280+
├─ .gjc/{specs,skills}/ # Agent 컨텍스트
281+
├─ middleware.ts # OG bypass · 로그인 redirect · E2E bypass
282+
├─ next.config.ts # serverActions.bodySizeLimit: "20mb"
283+
└─ vitest.config.ts · playwright.config.ts · tailwind.config.ts · ...
284+
```
285+
286+
## 5. 핵심 요청 흐름 한 줄 정리
287+
288+
- **참여 기록 저장**
289+
브라우저 → Caddy → Next.js → `upsertParticipation` (Server Action) → `requireUser()``parseParticipationForm()` → Prisma → `revalidatePath` → 상세 페이지에 본인 행 표시.
290+
- **사진 업로드**
291+
브라우저 preflight(`checkUploadFile`) → Server Action → 서버 재검증(`upload-limits`) → `storeUploadedFile()``Session.groupPhotoPath` 갱신 → 다음 요청 때 `opengraph-image` 가 해당 경로 합성.
292+
- **관리자 승인**
293+
`/admin/members``requireAdmin()` → Prisma `User.update({ approved, approvedById, approvedAt, role })`.
294+
- **OAuth 로그인**
295+
`/login` → Auth.js v5 → Google OIDC (`hd: snu.ac.kr`) → 콜백 → 화이트리스트 확인 → 미승인이면 `/pending`, 오류면 `/login?error=...` 한국어 안내.
296+
- **세션 수정/삭제**
297+
`/sessions/[id]/edit``requireHostOrAdmin()``updateSession` / `deleteSession` (cascade) + `deleteUploadedFile()`.
298+
299+
## 6. 배포 흐름
300+
301+
- `main` push → CI(typecheck · lint · test · build) → GHCR `prod-<sha>` 푸시 → N100 SSH → `docker compose pull && up -d` → post-deploy smoke(`/api/health` 200, `/login?error=Configuration` 한국어).
302+
- `dev` push → CI + GHCR `staging-<sha>` 푸시까지만, N100 미배포 (`deploy.yml` 의 분기).
303+
- 수동: `workflow_dispatch``prod` 만 허용.
304+
305+
## 7. 환경별 SSOT (Single Source of Truth)
306+
307+
| 영역 | 위치 |
308+
|------|------|
309+
| 데이터 모델 | `prisma/schema.prisma` |
310+
| 권한 매트릭스 | [`03-Screen-Flow §5`](03-Screen-Flow#5-권한-매트릭스) |
311+
| 업로드 제약 (15MB · MIME) | `lib/upload-limits.ts` |
312+
| 화면 흐름 / 페이지 목록 | [`03-Screen-Flow`](03-Screen-Flow) |
313+
| 기술 결정 근거 | [`02-Tech-Stack`](02-Tech-Stack) |
314+
| Agent 작업 절차 | `.gjc/skills/murun-feature/SKILL.md` |
315+
| 배포 정책 | `deploy/README.md` · `.github/workflows/deploy.yml` |
316+
| 보류 결정과 후속 TODO | [`09-Backlog-and-Decisions`](09-Backlog-and-Decisions) |

docs/wiki/Home.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [01 · 프로젝트 기획서](01-Project-Plan) — 서비스 목표, 타깃, 핵심 기능
99
- [02 · 기술 스택 선택과 이유](02-Tech-Stack)
1010
- [03 · 화면 흐름 / 데이터 모델 초안](03-Screen-Flow)
11+
- [아키텍처 개요](Architecture-Overview)
1112

1213
### Agent 개발 워크플로우
1314
- [04 · Agent 개발 workflow](04-Agent-Workflow)

0 commit comments

Comments
 (0)