From 8d8cc24ca7ec7907fd38254e90b3b232da64786e Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 02:59:21 +0900 Subject: [PATCH 01/14] =?UTF-8?q?fix:=20=EB=A1=9C=EC=BB=AC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EC=A0=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/worktrees/availability-aggregation | 1 - .env | 14 -------------- .gitignore | 1 + 3 files changed, 1 insertion(+), 15 deletions(-) delete mode 160000 .claude/worktrees/availability-aggregation delete mode 100644 .env diff --git a/.claude/worktrees/availability-aggregation b/.claude/worktrees/availability-aggregation deleted file mode 160000 index fdbed67..0000000 --- a/.claude/worktrees/availability-aggregation +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fdbed6717ea897329dfa003ea2577a6bd7d3f8eb diff --git a/.env b/.env deleted file mode 100644 index d152b92..0000000 --- a/.env +++ /dev/null @@ -1,14 +0,0 @@ -# 데이터베이스 및 API (Supabase) -NEXT_PUBLIC_SUPABASE_URL=https://dummy-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here -SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here - -# 캘린더 연동용 OAuth Client (Google/Apple) -GOOGLE_CLIENT_ID=your_google_client_id_here -GOOGLE_CLIENT_SECRET=your_google_client_secret_here - -# Gemini API (이미지 분석용) -GEMINI_API_KEY=your_gemini_api_key_here - -# 기타 연동 시 필요한 기본 주소 포트 설정 등 -NEXT_PUBLIC_BASE_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore index 3b3ff38..6bcb5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ yarn-error.log* next-env.d.ts # AI & Local configs +.claude/ .agents/* !.agents/docs/ *.pdf From fcee40acdc6c28efeb480b1b3a13a4c724461b4d Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:00:29 +0900 Subject: [PATCH 02/14] =?UTF-8?q?chore:=20CI=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 16 +++++++++++++++- .github/workflows/pr-compliance.yml | 28 +--------------------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97e8ebf..245a44d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,13 @@ on: branches: - main - dev + push: + branches: + - main + - dev jobs: - build_and_lint: + code_quality: runs-on: ubuntu-latest steps: @@ -18,6 +22,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' + cache: 'npm' - name: 패키지 설치 (Install Dependencies) run: npm ci @@ -25,5 +30,14 @@ jobs: - name: 코드 린트 검사 (Lint Check) run: npm run lint + - name: 테스트 실행 (Unit Test) + run: npm run test + - name: Next.js 빌드 테스트 (Build Test) run: npm run build + + - name: Playwright 브라우저 설치 (Chromium) + run: npx playwright install --with-deps chromium + + - name: E2E 테스트 실행 (Chromium) + run: npm run test:e2e -- --project=chromium diff --git a/.github/workflows/pr-compliance.yml b/.github/workflows/pr-compliance.yml index 6f92673..cc1fff5 100644 --- a/.github/workflows/pr-compliance.yml +++ b/.github/workflows/pr-compliance.yml @@ -1,11 +1,10 @@ -name: PR 품질 검사 (Compliance + Code Quality) +name: PR 템플릿 검사 on: pull_request: types: [opened, edited, synchronize] jobs: - # 1. PR 템플릿 양식 검사 check-template: runs-on: ubuntu-latest steps: @@ -26,28 +25,3 @@ jobs: fi echo "✅ PR 템플릿 검사 통과 성공!" - - # 2. 코드 품질 검증 (린트 → 테스트 → 빌드) - code-quality: - runs-on: ubuntu-latest - steps: - - name: 📥 코드 체크아웃 - uses: actions/checkout@v4 - - - name: 🟢 Node.js 설치 - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: 📦 의존성 설치 - run: npm ci - - - name: 🔍 린트 검사 - run: npm run lint - - - name: 🧪 테스트 실행 - run: npm run test - - - name: 🏗️ 빌드 테스트 - run: npm run build From 1f8b880258a1ea4b585777cb2f75ff4ed0e229e4 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:02:32 +0900 Subject: [PATCH 03/14] =?UTF-8?q?refactor:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=A0=80=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EB=AA=85=EB=AA=85=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/schedules/[id]/availability/route.ts | 2 +- src/app/api/schedules/[id]/route.ts | 2 +- src/app/api/schedules/route.ts | 6 +-- .../__tests__/in-memory-store.test.ts} | 18 +++---- .../store.ts => schedules/in-memory-store.ts} | 54 ++++++++++--------- 5 files changed, 42 insertions(+), 40 deletions(-) rename src/lib/{schedule-test/__tests__/store.test.ts => schedules/__tests__/in-memory-store.test.ts} (90%) rename src/lib/{schedule-test/store.ts => schedules/in-memory-store.ts} (83%) diff --git a/src/app/api/schedules/[id]/availability/route.ts b/src/app/api/schedules/[id]/availability/route.ts index b39b479..00da8e3 100644 --- a/src/app/api/schedules/[id]/availability/route.ts +++ b/src/app/api/schedules/[id]/availability/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { addParticipantAvailability, getSchedulePublic, -} from "@/lib/schedule-test/store"; +} from "@/lib/schedules/in-memory-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/schedules/[id]/route.ts b/src/app/api/schedules/[id]/route.ts index ce781e8..1ded162 100644 --- a/src/app/api/schedules/[id]/route.ts +++ b/src/app/api/schedules/[id]/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getScheduleForHost, getSchedulePublic, -} from "@/lib/schedule-test/store"; +} from "@/lib/schedules/in-memory-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/schedules/route.ts b/src/app/api/schedules/route.ts index d1a3f6c..5984156 100644 --- a/src/app/api/schedules/route.ts +++ b/src/app/api/schedules/route.ts @@ -1,15 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; import { - createTestSchedule, + createSchedule, getSchedulePublic, -} from "@/lib/schedule-test/store"; +} from "@/lib/schedules/in-memory-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function POST(request: NextRequest) { try { - const created = createTestSchedule(await request.json()); + const created = createSchedule(await request.json()); const schedule = getSchedulePublic(created.id); return NextResponse.json( diff --git a/src/lib/schedule-test/__tests__/store.test.ts b/src/lib/schedules/__tests__/in-memory-store.test.ts similarity index 90% rename from src/lib/schedule-test/__tests__/store.test.ts rename to src/lib/schedules/__tests__/in-memory-store.test.ts index 751a213..3b31aaf 100644 --- a/src/lib/schedule-test/__tests__/store.test.ts +++ b/src/lib/schedules/__tests__/in-memory-store.test.ts @@ -2,19 +2,19 @@ import { describe, expect, test, afterEach } from "vitest"; import type { TimeSlot } from "@/types/schedule"; import { addParticipantAvailability, - clearTestSchedules, - createTestSchedule, + clearSchedules, + createSchedule, getScheduleForHost, getSchedulePublic, -} from "../store"; +} from "../in-memory-store"; -describe("test schedule store", () => { +describe("in-memory schedule store", () => { afterEach(() => { - clearTestSchedules(); + clearSchedules(); }); test("creates an unguessable schedule id and keeps the host token out of public data", () => { - const created = createTestSchedule({ + const created = createSchedule({ title: "제품 회의", durationMinutes: 60, candidateDays: ["MON", "TUE"], @@ -37,7 +37,7 @@ describe("test schedule store", () => { }); test("requires the host token before returning participant details and common slots", () => { - const created = createTestSchedule({ + const created = createSchedule({ title: "인터뷰 일정", durationMinutes: 60, candidateDays: ["MON"], @@ -69,7 +69,7 @@ describe("test schedule store", () => { }); test("rejects participant slots outside the schedule candidate window", () => { - const created = createTestSchedule({ + const created = createSchedule({ title: "저녁 약속", durationMinutes: 60, candidateDays: ["FRI"], @@ -91,7 +91,7 @@ describe("test schedule store", () => { }); test("rejects empty participant names and empty availability", () => { - const created = createTestSchedule({ + const created = createSchedule({ title: "스터디", durationMinutes: 60, candidateDays: ["SAT"], diff --git a/src/lib/schedule-test/store.ts b/src/lib/schedules/in-memory-store.ts similarity index 83% rename from src/lib/schedule-test/store.ts rename to src/lib/schedules/in-memory-store.ts index a86f5cd..4277dc9 100644 --- a/src/lib/schedule-test/store.ts +++ b/src/lib/schedules/in-memory-store.ts @@ -7,7 +7,7 @@ import type { import { findCommonSlots } from "@/lib/scheduling/availability"; import { mergeOverlapping, sortSlots } from "@/lib/scheduling/time-slot"; -export interface CreateTestScheduleInput { +export interface CreateInMemoryScheduleInput { title: string; durationMinutes: number; candidateDays: DayCode[]; @@ -20,12 +20,12 @@ export interface AddParticipantAvailabilityInput { available: TimeSlot[]; } -export interface CreatedTestSchedule { +export interface CreatedInMemorySchedule { id: string; hostToken: string; } -export interface PublicTestSchedule { +export interface PublicSchedule { id: string; title: string; durationMinutes: number; @@ -36,19 +36,19 @@ export interface PublicTestSchedule { createdAt: string; } -export interface TestParticipant { +export interface ScheduleParticipant { id: string; name: string; available: TimeSlot[]; submittedAt: string; } -export interface HostTestSchedule extends PublicTestSchedule { - participants: TestParticipant[]; +export interface HostSchedule extends PublicSchedule { + participants: ScheduleParticipant[]; commonSlots: TimeSlot[]; } -interface TestScheduleRecord { +interface ScheduleRecord { id: string; hostToken: string; title: string; @@ -56,20 +56,20 @@ interface TestScheduleRecord { candidateDays: DayCode[]; candidateStartHour: number; candidateEndHour: number; - participants: TestParticipant[]; + participants: ScheduleParticipant[]; createdAt: string; } -interface TestScheduleStore { - schedules: Map; +interface ScheduleStore { + schedules: Map; } -const STORE_KEY = "__moimTestScheduleStore"; +const STORE_KEY = "__moimInMemoryScheduleStore"; const VALID_DAYS: DayCode[] = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]; -export function createTestSchedule( - input: CreateTestScheduleInput, -): CreatedTestSchedule { +export function createSchedule( + input: CreateInMemoryScheduleInput, +): CreatedInMemorySchedule { const schedule = normalizeScheduleInput(input); const id = createToken(18); const hostToken = createToken(32); @@ -85,7 +85,7 @@ export function createTestSchedule( return { id, hostToken }; } -export function getSchedulePublic(id: string): PublicTestSchedule | null { +export function getSchedulePublic(id: string): PublicSchedule | null { const schedule = getStore().schedules.get(id); if (!schedule) return null; return toPublicSchedule(schedule); @@ -94,7 +94,7 @@ export function getSchedulePublic(id: string): PublicTestSchedule | null { export function getScheduleForHost( id: string, hostToken: string, -): HostTestSchedule | null { +): HostSchedule | null { const schedule = getStore().schedules.get(id); if (!schedule || !tokenMatches(schedule.hostToken, hostToken)) return null; @@ -114,11 +114,11 @@ export function getScheduleForHost( export function addParticipantAvailability( scheduleId: string, input: AddParticipantAvailabilityInput, -): TestParticipant { +): ScheduleParticipant { const schedule = getStore().schedules.get(scheduleId); if (!schedule) throw new Error("schedule not found"); - const participant: TestParticipant = { + const participant: ScheduleParticipant = { id: createToken(12), name: normalizeParticipantName(input.name), available: normalizeAvailability(schedule, input.available), @@ -129,13 +129,13 @@ export function addParticipantAvailability( return copyParticipant(participant); } -export function clearTestSchedules() { +export function clearSchedules() { getStore().schedules.clear(); } -function getStore(): TestScheduleStore { +function getStore(): ScheduleStore { const globalStore = globalThis as typeof globalThis & { - [STORE_KEY]?: TestScheduleStore; + [STORE_KEY]?: ScheduleStore; }; if (!globalStore[STORE_KEY]) { @@ -146,8 +146,8 @@ function getStore(): TestScheduleStore { } function normalizeScheduleInput( - input: CreateTestScheduleInput, -): Omit { + input: CreateInMemoryScheduleInput, +): Omit { const title = input.title.trim(); if (title.length < 2 || title.length > 80) { throw new Error("title must be between 2 and 80 characters"); @@ -193,7 +193,7 @@ function normalizeParticipantName(name: string): string { } function normalizeAvailability( - schedule: TestScheduleRecord, + schedule: ScheduleRecord, available: TimeSlot[], ): TimeSlot[] { if (available.length === 0) { @@ -226,7 +226,7 @@ function validateHourRange(startHour: number, endHour: number) { } } -function toPublicSchedule(schedule: TestScheduleRecord): PublicTestSchedule { +function toPublicSchedule(schedule: ScheduleRecord): PublicSchedule { return { id: schedule.id, title: schedule.title, @@ -239,7 +239,9 @@ function toPublicSchedule(schedule: TestScheduleRecord): PublicTestSchedule { }; } -function copyParticipant(participant: TestParticipant): TestParticipant { +function copyParticipant( + participant: ScheduleParticipant, +): ScheduleParticipant { return { ...participant, available: participant.available.map((slot) => ({ ...slot })), From 83cec4f0f1eb3b88d8c34a953551ffc88ae47d0f Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:08:35 +0900 Subject: [PATCH 04/14] =?UTF-8?q?chore:=20=EC=98=A4=EB=9E=98=EB=90=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/codex-work-context.md | 210 ++--- docs/provided-documents-summary.md | 8 +- .../2026-04-27-moim-user-test-prototype.md | 777 ------------------ .../schedule/AvailabilityResult.tsx | 26 - src/components/schedule/ParticipantList.tsx | 21 - src/components/schedule/TimeGrid.tsx | 25 - 6 files changed, 54 insertions(+), 1013 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md delete mode 100644 src/components/schedule/AvailabilityResult.tsx delete mode 100644 src/components/schedule/ParticipantList.tsx delete mode 100644 src/components/schedule/TimeGrid.tsx diff --git a/docs/codex-work-context.md b/docs/codex-work-context.md index d0ae25f..d35f885 100644 --- a/docs/codex-work-context.md +++ b/docs/codex-work-context.md @@ -1,21 +1,19 @@ # MOIM 개발 현황 인수인계 -작성일: 2026-05-14 +작성일: 2026-05-28 이 문서는 새 Codex 세션이 `C:\Users\kksu1\Dev\MOIM`에서 바로 맥락을 잡기 위한 작업 현황 노트다. 제품 설명서가 아니라, 현재 어디까지 왔고 무엇을 조심해야 하는지 정리한 handoff 문서다. ## 먼저 읽을 파일 -1. `docs/codex-work-context.md` - - 현재 개발 상태, 열린 PR, 다음 작업 순서를 빠르게 파악한다. -2. `docs/user-flow.md` +1. `docs/user-flow.md` - 실제 제품 흐름의 기준 문서다. 핵심 방향은 게스트 퍼스트, 초대 링크 문맥 우선이다. -3. `docs/superpowers/specs/2026-04-28-moim-role-based-user-test-prototype-design.md` - - 사용자 테스트용 역할 기반 프로토타입의 승인된 설계 문서다. -4. `docs/superpowers/plans/2026-04-28-moim-role-based-user-test-prototype.md` - - 역할 기반 프로토타입 구현 계획과 검증 흐름이 들어 있다. -5. `prototype/moim.html` - - 현재 사용자 테스트용 단일 HTML 프로토타입의 중심 파일이다. +2. `convention.md` + - 커밋, 브랜치, 이슈, PR 규칙의 기준이다. +3. `ARCHITECTURE.md` + - 코드 구조와 테스트 전략을 볼 때의 기준 문서다. +4. `prototype/moim.html` + - 사용자 테스트용 단일 HTML 프로토타입의 중심 파일이다. ## 제품 방향 @@ -24,8 +22,7 @@ MOIM은 여러 사람이 모임 시간을 정할 때, 주최자가 링크를 만 현재 가장 중요한 제품 원칙은 다음과 같다. - 실제 제품 흐름은 `게스트 퍼스트 + 초대 링크 문맥 우선`이다. -- `/join/:id`처럼 초대 링크로 들어온 사용자는 바로 참여자 흐름으로 들어가야 한다. -- 일반 홈 `/`은 `모임 만들기`와 `초대 링크로 참여`를 중심으로 둔다. +- 초대 링크로 들어온 사용자는 바로 참여자 흐름으로 들어가야 한다. - 로그인, 캘린더 저장, 계정 저장은 첫 관문이 아니라 가치를 경험한 뒤 제안한다. - 사용자 테스트용 역할 허브는 테스트 장치다. 실제 제품 첫 화면으로 쓰면 안 된다. @@ -39,50 +36,25 @@ MVP의 최소 흐름은 다음 순서가 기준이다. ## 현재 repo 상태 -2026-05-14 확인 기준: +2026-05-28 확인 기준: - repo root: `C:\Users\kksu1\Dev\MOIM` -- 현재 로컬 브랜치: `feat/prototype-user-test` -- 현재 HEAD: `d8e5dd1 feat: 역할 기반 사용자 테스트 프로토타입 개선` - GitHub remote: `origin https://github.com/Siul49/moim.git` - 기준 통합 브랜치: `dev` +- PR #16, #21, #26, #27, #29는 `dev`에 merge 완료 +- 현재 레포 위생 정리 작업 이슈: #30 -현재 작업트리는 깨끗하지 않다. 다음 세션은 아래 변경을 사용자 작업으로 보고 함부로 되돌리면 안 된다. +작업 시작 전에는 항상 아래를 다시 확인한다. -수정됨: - -- `.agents/docs/1_DOMAIN_CONTEXT.md` -- `.agents/docs/2_EXECUTION_PLAN.md` - -미추적: - -- `.claude/` -- `docs/provided-documents-summary.md` -- `docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md` -- `docs/user-flow.md` -- `tsconfig.json` -- `docs/codex-work-context.md` - -특히 `.agents/docs/*`와 `.claude/`는 다른 작업 흐름의 산출물일 수 있으므로, 명시 요청 없이 정리하지 않는다. +```powershell +git fetch origin --prune +git status --short --branch +gh pr list --state open --json number,title,headRefName,baseRefName,url +``` ## 현재 구현 축 -### 1. 사용자 테스트용 정적 프로토타입 - -중심 파일: - -- `prototype/moim.html` - -역할 기반 사용자 테스트 프로토타입은 네 관점을 한 파일에서 볼 수 있게 하는 방향이다. - -- 게스트 -- 로그인 유저 -- 모임 만드는 사람 -- 초대받아 참여하는 사람 - -이 역할 허브는 실제 제품의 정보 구조가 아니라 테스트용 진입 장치다. 실제 제품 첫 화면은 `docs/user-flow.md`의 게스트 퍼스트 흐름을 따른다. - -### 2. Next.js 앱 +### 1. Next.js 앱 프로젝트는 Next.js App Router 기반이다. @@ -90,150 +62,72 @@ MVP의 최소 흐름은 다음 순서가 기준이다. ```powershell npm ci -npm test npm run lint +npm run test npm run build -npm run test:e2e +npm run test:e2e -- --project=chromium ``` `package.json` 기준 주요 스크립트: - `npm run dev`: Next dev server -- `npm test`: Vitest +- `npm run test`: Vitest - `npm run lint`: Next lint - `npm run build`: production build - `npm run test:e2e`: Playwright -## 열린 PR 현황 - -2026-05-14 확인 기준 열린 PR은 2개다. - -### PR #16 +### 2. 일정 생성/참여 플로우 -- URL: https://github.com/Siul49/moim/pull/16 -- 제목: `feat(scheduling): 캘린더 통합 가용시간 산출 프레임워크 (Date 기반 어댑터)` -- base: `dev` -- head: `feature/15-availability-aggregation` -- GitHub merge 상태: `MERGEABLE`, `CLEAN` -- checks: 통과 -- review decision: `CHANGES_REQUESTED` +현재 앱에는 Supabase 저장소와 별개로 브라우저 테스트 가능한 일정 생성/참여 플로우가 있다. -로컬 검증 결과: +- `src/app/schedule/create/CreateScheduleClient.tsx`: 호스트가 모임을 만들고 참여자/호스트 링크를 받는다. +- `src/app/schedule/[id]/ScheduleRoomClient.tsx`: 참여자가 이름과 가능 시간을 제출하고, host token이 있으면 결과를 본다. +- `src/app/api/schedules/*`: 위 플로우를 위한 API route다. +- `src/lib/schedules/in-memory-store.ts`: 프로토타입/E2E용 in-memory schedule store다. -- `npm test`: 16 files, 125 tests 통과 -- `npm run lint`: 통과 -- `npm run build`: 통과 +이 저장소는 운영 영속 저장소가 아니다. Supabase-backed schedule persistence를 붙일 때는 API contract를 유지하면서 저장소 구현을 교체한다. -머지 판단: +### 3. 캘린더/시간 계산 -- 충돌이나 기계적 실패는 없다. -- 다만 CodeRabbit이 남긴 Date 객체 aliasing, Google all-day parsing, manual adapter purity 같은 리뷰는 아직 남아 있다. -- 기반 scheduling 유틸이므로, 바로 머지하기보다는 작은 방어 수정 후 머지하는 편이 좋다. +기준 모듈: -우선 확인할 파일: - -- `src/lib/scheduling/time-slot.ts` +- `src/lib/scheduling/availability.ts` - `src/lib/scheduling/free-slots.ts` -- `src/lib/calendar/adapters/google.ts` -- `src/lib/calendar/adapters/manual.ts` - -### PR #21 - -- URL: https://github.com/Siul49/moim/pull/21 -- 제목: `feat: 에브리타임 시간표 조회 기능 구현` -- base: `dev` -- head: `feature/20-everytime-timetable` -- GitHub merge 상태: `MERGEABLE`, `CLEAN` -- checks: 통과 -- review decision: `CHANGES_REQUESTED` - -로컬 검증 결과: - -- `npm test`: 15 files, 123 tests 통과 -- `npm run lint`: 통과 -- `npm run build`: 통과 - -머지 판단: - -- 충돌이나 기계적 실패는 없다. -- 실제 외부 입력을 받는 API라서 머지 전 보강이 더 중요하다. -- 특히 ICS 업로드 크기 제한, 내용 기반 ICS 검증, 시간 값 범위 검증, URL 입력 검증을 먼저 보는 것이 좋다. - -우선 확인할 파일: +- `src/lib/scheduling/time-slot.ts` +- `src/lib/calendar/adapters/*` +- `src/lib/scheduling/ics-parser.ts` -- `src/app/api/everytime/timetable/route.ts` -- `src/lib/everytime/url-scraper.ts` -- `src/lib/everytime/timetable.ts` -- `src/lib/everytime/ics-converter.ts` +Google/iCloud/수동 입력/ICS는 provider raw data를 표준 시간 표현으로 바꾼 뒤 공통 가용시간 계산으로 들어가는 구조다. -### 두 PR 동시 머지 검증 +### 4. 인증/계정 -임시 worktree에서 `origin/dev` 위에 PR #16, PR #21을 순서대로 merge 했을 때: +현재 `dev`에는 이메일/닉네임 로그인, 회원가입, 카카오 로그인, Supabase/Prisma 관련 작업이 merge되어 있다. host auth와 participant guest flow는 분리해서 다룬다. -- merge conflict 없음 -- `npm test`: 21 files, 157 tests 통과 -- `npm run lint`: 통과 -- `npm run build`: 통과 +## 레포 위생 기준 -즉 통합 자체는 가능하지만, review gate는 아직 남아 있다. +- `.env`는 GitHub에 올리지 않는다. 필요한 키 이름만 `.env.example`에 둔다. +- `.claude/`, worktree, 로컬 실행 로그, 테스트 결과물은 추적하지 않는다. +- agent 전용 계획 문서는 기본적으로 레포에 남기지 않는다. 사람이 읽는 제품/개발 문서는 `docs/`에 직접 정리한다. +- 오래된 스켈레톤 파일은 실제 구현과 연결되어 있지 않으면 삭제한다. ## 다음 작업 추천 순서 -1. 현재 작업트리 상태를 다시 확인한다. - -```powershell -git status --short --branch -gh pr list --state open --json number,title,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,url -``` - -2. 사용자가 PR 정리를 원하면 PR #21부터 보는 것을 추천한다. - -이유: - -- Everytime API는 외부 URL, XML, ICS 파일을 직접 다룬다. -- validation 누락이 실제 운영 리스크로 이어질 가능성이 PR #16보다 높다. -- checks는 통과하지만 review decision은 아직 `CHANGES_REQUESTED`다. - -3. PR #21 보강 시 최소 성공 기준: - -- ICS 파일 크기 제한을 둔다. -- 파일명/MIME만 보지 말고 `BEGIN:VCALENDAR`, `END:VCALENDAR` 같은 내용 기반 확인을 한다. -- `startMinute`, `endMinute`, `day`는 finite number, 정수, 범위 조건을 검증한다. -- `timetableToFreeSlots` 오류와 scrape/parse 오류를 구분해서 응답한다. -- `npm test`, `npm run lint`, `npm run build`를 통과시킨다. - -4. PR #16 보강 시 최소 성공 기준: - -- `mergeOverlappingDateSlots`가 입력 Date 객체를 결과에 그대로 공유하지 않게 한다. -- `busyEventsToFree`의 empty busy path도 window Date 객체를 복사해서 반환한다. -- Google all-day parsing의 서버 로컬 timezone 의존성을 줄인다. -- `npm test`, `npm run lint`, `npm run build`를 통과시킨다. - -5. 프로토타입 작업을 이어가면 `prototype/moim.html`과 `e2e/prototype-flow.spec.ts`를 함께 본다. - -현재 역할 기반 프로토타입 문서의 방향은: - -- 첫 화면은 사용자 테스트용 역할 허브 -- 실제 제품 첫 화면은 게스트 퍼스트 -- 참여자는 로그인 없이 제출 가능 -- 주최자는 빠르게 링크를 만들고 공유 가능 -- AI는 시간을 자동 확정하지 않고 추천만 한다 +1. 이슈 #30의 레포 위생/CI 정리를 완료하고 PR을 만든다. +2. `dev` 기준 CI가 `lint`, `test`, `build`, Chromium e2e를 모두 실행하는지 확인한다. +3. Supabase-backed schedule persistence를 별도 이슈로 분리한다. +4. 참여자 guest flow와 host auth flow가 섞이지 않도록 API와 UI 경계를 유지한다. ## 다음 세션 시작 프롬프트 -새 세션에서 바로 이어가려면 아래처럼 시작하면 된다. - ```text C:\Users\kksu1\Dev\MOIM에서 작업해줘. -먼저 docs/codex-work-context.md를 읽고 현재 개발 현황을 파악해. -기존 작업트리에 수정/미추적 파일이 있으니 함부로 되돌리지 말고, git status와 열린 PR 상태를 다시 확인해. -우선순위는 PR #21 Everytime 시간표 기능의 merge-blocking 리뷰 보강이고, 필요하면 PR #16 scheduling 유틸 보강도 이어서 봐줘. -변경 후에는 npm test, npm run lint, npm run build로 검증해. +먼저 docs/codex-work-context.md와 convention.md를 읽고 현재 개발 현황과 협업 규칙을 파악해. +기존 작업트리에 수정/미추적 파일이 있으면 함부로 되돌리지 말고, git status와 GitHub PR 상태를 다시 확인해. +변경 후에는 npm run lint, npm run test, npm run build, 가능하면 npm run test:e2e -- --project=chromium으로 검증해. ``` ## 주의할 점 -- 이 문서는 2026-05-14 시점의 snapshot이다. PR 상태와 CI 결과는 바뀔 수 있으므로 다음 세션에서 반드시 다시 확인한다. -- 기존 문서 중 일부는 PowerShell 출력에서 mojibake처럼 보일 수 있다. 문서 내용을 판단할 때는 `-Encoding UTF8`로 다시 읽는다. -- 로컬 `node_modules`가 현재 브랜치와 맞지 않으면 테스트가 잘못 실패할 수 있다. PR 검증은 깨끗한 worktree에서 `npm ci`부터 실행하는 편이 안전하다. +- 이 문서는 snapshot이다. PR 상태와 CI 결과는 바뀔 수 있으므로 다음 세션에서 반드시 다시 확인한다. +- PowerShell에서 한글이 깨져 보이면 파일 문제라고 단정하지 말고 `Get-Content -Raw -Encoding UTF8`로 다시 읽는다. - 문서/프로토타입은 한국어 톤을 유지한다. 과한 corporate wording보다 학생 팀이 실제로 설명하는 문장에 가깝게 쓴다. diff --git a/docs/provided-documents-summary.md b/docs/provided-documents-summary.md index fe14b28..cb64497 100644 --- a/docs/provided-documents-summary.md +++ b/docs/provided-documents-summary.md @@ -28,10 +28,8 @@ | 도메인 컨텍스트 | `.agents/docs/1_DOMAIN_CONTEXT.md` | 에이전트용 프로젝트 맥락 | 수정 중 | | 실행 계획 | `.agents/docs/2_EXECUTION_PLAN.md` | Phase별 구현 로드맵 | 수정 중 | | 파일 인덱스 | `.agents/docs/3_FILE_INDEX.md` | 파일 역할 지도 | 원본 MD 존재 | -| 사용자 테스트 프로토타입 설계 | `docs/superpowers/specs/2026-04-27-moim-user-test-prototype-design.md` | 게스트 퍼스트 프로토타입 설계 | 원본 MD 존재 | -| 역할 기반 프로토타입 설계 | `docs/superpowers/specs/2026-04-28-moim-role-based-user-test-prototype-design.md` | 역할 허브 기반 프로토타입 설계 | 원본 MD 존재 | -| 사용자 테스트 프로토타입 실행 계획 | `docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md` | 구현 체크리스트 | 현재 untracked | -| 역할 기반 프로토타입 실행 계획 | `docs/superpowers/plans/2026-04-28-moim-role-based-user-test-prototype.md` | 구현 체크리스트 | 원본 MD 존재 | +| 제품 사용자 흐름 문서 | `docs/user-flow.md` | 게스트 퍼스트 실제 제품 흐름 | 원본 MD 존재 | +| 개발 현황 인수인계 | `docs/codex-work-context.md` | 현재 개발 상태와 검증 기준 | 원본 MD 존재 | | AI calendar assistant PRD 리뷰 맥락 | 현재 원본 파일 미확인 | 제품/수익화/범위 리뷰 | 메모리 기반 맥락만 확인 | ## 사업계획서 핵심 내용 @@ -184,12 +182,10 @@ MOIM은 대학생 팀 활동에서 반복되는 일정 조율 문제를 줄이 - `.agents/docs/2_EXECUTION_PLAN.md`는 Phase 4를 캘린더 연동 구현 진행 중으로 보고 있다. - 별도 worktree에는 `feature/15-availability-aggregation` 브랜치의 캘린더 통합 가용시간 산출 프레임워크가 존재한다. - `feat/prototype-user-test` 브랜치에는 프로토타입 사용자 테스트 관련 문서와 계획이 남아 있다. -- `docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md`는 현재 untracked 상태라, 유지할 문서라면 명시적으로 추적 대상으로 추가해야 한다. ## 남은 문서 정리 이슈 - 원본 `PRD.md` 또는 `PRD.pdf`가 실제로 있었다면 현재 작업트리에 없다. 필요하면 원본을 다시 넣고 이 정리본에 “요구사항 원문” 섹션을 추가해야 한다. -- `docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md`는 일부 환경에서 한글이 깨져 보이는 내용이 포함되어 있다. 이 계획을 계속 쓸 거라면 UTF-8 정상 문구로 복구하는 편이 좋다. - `.agents/docs/1_DOMAIN_CONTEXT.md`와 `.agents/docs/2_EXECUTION_PLAN.md`도 콘솔 출력에서 깨져 보였다. 실제 파일 인코딩/내용을 확인하고, 필요하면 `docs/` 하위의 사람이 읽는 문서와 동기화해야 한다. - 사업계획서에 들어간 비교/효과 내용은 아직 실제 설문 수치 기반이 아니다. 프로토타입 테스트 뒤 평균 조율 시간, 메시지 수, 제출 완료율 같은 실제 수치로 보완해야 한다. diff --git a/docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md b/docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md deleted file mode 100644 index 32a2287..0000000 --- a/docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md +++ /dev/null @@ -1,777 +0,0 @@ -# MOIM User-Test Prototype Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Turn `prototype/moim.html` into a guest-first, user-test-ready prototype covering both host meeting creation and participant invite response flows. - -**Architecture:** Keep the current single-file prototype delivery model, but reorganize it around explicit screen states, guest-first entry, reusable save prompts, and simulated recovery/error states. Add a focused Playwright smoke spec that opens the static prototype via `file://` and verifies the most important host and participant paths across desktop and mobile. - -**Tech Stack:** Static HTML/CSS/JavaScript in `prototype/moim.html`, Playwright in `e2e/prototype-flow.spec.ts`, existing npm scripts and Playwright configuration. - ---- - -## File Structure - -- Modify `prototype/moim.html`: primary prototype UI, styling, state, simulated interactions, responsive behavior. -- Create `e2e/prototype-flow.spec.ts`: smoke coverage for guest-first home, host link creation, participant availability submission, save prompt dismissal, and mobile CTA visibility. -- Reference `docs/superpowers/specs/2026-04-27-moim-user-test-prototype-design.md`: approved design source of truth. - -The local `dev` branch currently has the approved spec commit, while `origin/dev` contains the first `prototype/moim.html` commit. Start by merging `origin/dev` into the local branch so the implementation includes both the spec and the prototype file. - ---- - -### Task 1: Synchronize Prototype Baseline - -**Files:** -- Modify through git merge: `prototype/moim.html` -- Preserve unrelated working changes: `.agents/docs/1_DOMAIN_CONTEXT.md`, `.agents/docs/2_EXECUTION_PLAN.md` - -- [ ] **Step 1: Confirm branch divergence and dirty files** - -Run: - -```powershell -git status --short --branch -git log --oneline --left-right --graph dev...origin/dev -``` - -Expected: - -```text -## dev...origin/dev [ahead 1, behind 1] - M .agents/docs/1_DOMAIN_CONTEXT.md - M .agents/docs/2_EXECUTION_PLAN.md -< c7a0277 docs: 사용자 테스트 프로토타입 설계 문서 추가 -> f289732 feat: HTML 프로토타입 초기 작업물 추가 -``` - -- [ ] **Step 2: Merge the remote prototype commit** - -Run: - -```powershell -git merge --no-edit origin/dev -``` - -Expected: merge succeeds and adds `prototype/moim.html`. If git reports that `.agents/docs/*` would be overwritten, stop and ask the user before touching those files. - -- [ ] **Step 3: Verify prototype file exists** - -Run: - -```powershell -Test-Path prototype\moim.html -git ls-files prototype/moim.html -``` - -Expected: - -```text -True -prototype/moim.html -``` - -- [ ] **Step 4: Inspect baseline screen IDs** - -Run: - -```powershell -Select-String -Path prototype\moim.html -Pattern 'id="s-|function go|function loginEmail|function submitP' -``` - -Expected: output includes the existing screen IDs `s-landing`, `s-calendar-setup`, `s-main`, `s-host-create`, `s-host-dates`, `s-host-share`, `s-status`, `s-participant`, `s-confirm`, `s-result`, `s-profile`. - ---- - -### Task 2: Add Failing Prototype Smoke Tests - -**Files:** -- Create: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Write static prototype Playwright tests** - -Create `e2e/prototype-flow.spec.ts` with this content: - -```ts -import { expect, test } from "@playwright/test"; -import { pathToFileURL } from "node:url"; -import path from "node:path"; - -const prototypeUrl = pathToFileURL( - path.join(process.cwd(), "prototype", "moim.html"), -).toString(); - -test.describe("MOIM 사용자 테스트용 HTML 프로토타입", () => { - test("홈은 로그인 게이트보다 모임 만들기와 초대 링크 참여를 먼저 보여준다", async ({ - page, - }) => { - await page.goto(prototypeUrl); - - await expect(page.getByRole("heading", { name: "모두의 빈 시간을 링크 하나로 찾기" })).toBeVisible(); - await expect(page.getByRole("button", { name: "모임 만들기" })).toBeVisible(); - await expect(page.getByRole("button", { name: "초대 링크로 참여" })).toBeVisible(); - await expect(page.getByText("로그인하면 다음부터 캘린더 연동 없이 시작할 수 있어요")).toBeVisible(); - }); - - test("주최자는 로그인 없이 모임을 만들고 초대 링크를 생성할 수 있다", async ({ page }) => { - await page.goto(prototypeUrl); - - await page.getByRole("button", { name: "모임 만들기" }).click(); - await expect(page.getByRole("heading", { name: "모임 기본 정보" })).toBeVisible(); - await page.getByRole("textbox", { name: "모임 이름" }).fill("조별과제 회의"); - await page.getByRole("button", { name: "다음" }).click(); - - await expect(page.getByRole("heading", { name: "내 일정 반영 방식" })).toBeVisible(); - await page.getByRole("button", { name: "연동 없이 계속" }).click(); - await page.getByRole("button", { name: "후보 시간 선택으로 이동" }).click(); - - await expect(page.getByRole("heading", { name: "후보 날짜와 시간" })).toBeVisible(); - await page.getByRole("button", { name: "링크 생성" }).click(); - - await expect(page.getByText("계정에 저장하지 않아도 링크를 만들 수 있어요")).toBeVisible(); - await page.getByRole("button", { name: "계속해서 링크 생성" }).click(); - - await expect(page.getByRole("heading", { name: "초대 링크가 만들어졌어요" })).toBeVisible(); - await expect(page.getByText("moim.ai/join/abc123xyz")).toBeVisible(); - await expect(page.getByText("다음 모임은 바로 만들 수 있어요")).toBeVisible(); - }); - - test("참여자는 로그인 없이 초대 링크에서 가능 시간을 제출할 수 있다", async ({ page }) => { - await page.goto(`${prototypeUrl}#join`); - - await expect(page.getByRole("heading", { name: "이 모임에 참여할 수 있는 시간을 알려주세요" })).toBeVisible(); - await expect(page.getByText("로그인 없이 제출 가능")).toBeVisible(); - await page.getByRole("button", { name: "직접 입력" }).click(); - - await expect(page.getByRole("heading", { name: "가능 시간 확인" })).toBeVisible(); - await page.getByRole("button", { name: "화 4/15 14:00 가능" }).click(); - await page.getByRole("button", { name: "제출하기" }).click(); - - await expect(page.getByText("계정 없이도 제출할 수 있어요")).toBeVisible(); - await page.getByRole("button", { name: "그냥 제출" }).click(); - - await expect(page.getByRole("heading", { name: "가능 시간이 제출됐어요" })).toBeVisible(); - await expect(page.getByText("다음 초대에서도 자동으로 응답하려면 저장할까요?")).toBeVisible(); - }); - - test("참여자는 사진 분석 실패 후 직접 입력으로 회복할 수 있다", async ({ page }) => { - await page.goto(`${prototypeUrl}#join`); - - await page.getByRole("button", { name: "사진 업로드" }).click(); - await expect(page.getByText("AI가 시간표를 분석하고 있어요")).toBeVisible(); - await page.getByRole("button", { name: "분석 실패 상태 보기" }).click(); - await expect(page.getByText("시간표를 읽지 못했어요")).toBeVisible(); - await page.getByRole("button", { name: "직접 입력으로 계속" }).click(); - await expect(page.getByRole("heading", { name: "가능 시간 확인" })).toBeVisible(); - }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail against the baseline** - -Run: - -```powershell -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -``` - -Expected: FAIL because the baseline home is login-first and does not expose the new headings/buttons. - ---- - -### Task 3: Implement Guest-First Home And Entry Routing - -**Files:** -- Modify: `prototype/moim.html` -- Test: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Replace the login-first landing screen** - -In `prototype/moim.html`, replace the visible contents of `#s-landing` with a guest-first screen that exposes these accessible targets: - -```html -

MOIM

-

모두의 빈 시간을 링크 하나로 찾기

-

캘린더 연동, 사진 업로드, 직접 입력 중 편한 방식으로 모임 시간을 빠르게 맞춰요.

- - - - -``` - -- [ ] **Step 2: Add entry state helpers** - -Add these functions near the existing navigation helpers: - -```html - -``` - -If `prototypeState` conflicts with script placement, keep one global object and do not duplicate it. - -- [ ] **Step 3: Route `#join` directly to participant flow** - -In the `DOMContentLoaded` handler, add: - -```js -if (window.location.hash === "#join") { - startParticipantFlow(); -} else { - go("s-landing", false); -} -``` - -Expected behavior: opening `prototype/moim.html#join` shows the participant invite screen. - -- [ ] **Step 4: Run the home test** - -Run: - -```powershell -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -g "홈은 로그인 게이트" -``` - -Expected: PASS. - ---- - -### Task 4: Rework Host Flow For Optional Calendar Setup And Link Creation - -**Files:** -- Modify: `prototype/moim.html` -- Test: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Rename and simplify host meeting basics** - -In `#s-host-create`, make the screen heading accessible as: - -```html -

모임 기본 정보

-``` - -Ensure the meeting-name field has a real label: - -```html - - -``` - -Keep duration, participant count, and purpose chips from the existing prototype. - -- [ ] **Step 2: Send the host to calendar setup after basics** - -Change the primary next button in `#s-host-create` to: - -```html - -``` - -- [ ] **Step 3: Reframe calendar setup as optional** - -In `#s-calendar-setup`, change the heading to: - -```html -

내 일정 반영 방식

-

캘린더를 연동하면 자동으로 막힌 시간이 반영돼요. 지금은 건너뛰고 직접 입력해도 됩니다.

-``` - -Add explicit recovery buttons: - -```html - - - -``` - -- [ ] **Step 4: Add calendar setup state functions** - -Add: - -```js -function simulateCalendarRejected() { - prototypeState.calendarState = "rejected"; - const notice = document.getElementById("calendar-state-notice"); - if (notice) { - notice.className = "notice notice-red"; - notice.textContent = "캘린더 권한이 거부됐어요. 다시 시도하거나 직접 입력으로 계속할 수 있어요."; - } -} - -function skipCalendarSetup() { - prototypeState.calendarState = "skipped"; - const notice = document.getElementById("calendar-state-notice"); - if (notice) { - notice.className = "notice notice-purple"; - notice.textContent = "연동 없이 계속합니다. 후보 시간은 직접 조정할 수 있어요."; - } -} -``` - -Also add a target element inside `#s-calendar-setup`: - -```html -
로그인 없이도 계속할 수 있어요.
-``` - -- [ ] **Step 5: Add pre-link soft save prompt** - -In the link generation action from `#s-host-dates`, show a soft prompt instead of navigating immediately: - -```js -function showHostLinkSavePrompt() { - prototypeState.authSavePrompt = "soft"; - const modal = document.getElementById("soft-save-modal"); - modal.querySelector("[data-save-title]").textContent = "계정에 저장하지 않아도 링크를 만들 수 있어요"; - modal.querySelector("[data-save-body]").textContent = "저장하면 다음 모임부터 캘린더 연동을 다시 하지 않아도 됩니다."; - modal.querySelector("[data-save-primary]").textContent = "저장하고 링크 생성"; - modal.querySelector("[data-save-secondary]").textContent = "계속해서 링크 생성"; - modal.querySelector("[data-save-secondary]").onclick = finishHostLinkCreation; - modal.style.display = "flex"; -} - -function finishHostLinkCreation() { - document.getElementById("soft-save-modal").style.display = "none"; - prototypeState.authSavePrompt = "strong"; - prototypeState.meetingState = "linkCreated"; - go("s-host-share"); -} -``` - -Add a reusable modal near the end of `body`: - -```html - -``` - -- [ ] **Step 6: Update host share completion copy** - -In `#s-host-share`, use: - -```html -

초대 링크가 만들어졌어요

- -
-

다음 모임은 바로 만들 수 있어요

-

이 캘린더 연동 정보를 계정에 저장하면 다음부터 같은 설정을 반복하지 않아도 됩니다.

- - -
-``` - -Add: - -```js -function dismissStrongSavePrompt() { - prototypeState.authSavePrompt = "dismissed"; - showToast("저장하지 않고 계속합니다"); -} -``` - -- [ ] **Step 7: Run host flow test** - -Run: - -```powershell -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -g "주최자는 로그인 없이" -``` - -Expected: PASS. - ---- - -### Task 5: Rework Participant Invite And Availability Submission Flow - -**Files:** -- Modify: `prototype/moim.html` -- Test: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Replace participant entry hierarchy** - -In `#s-participant`, ensure the main heading and reassurance are: - -```html -

이 모임에 참여할 수 있는 시간을 알려주세요

-

조별과제 회의 · 1시간 · 후보 3일 · 마감 4/16

-로그인 없이 제출 가능 -``` - -Add three response method buttons: - -```html - - - -``` - -- [ ] **Step 2: Add participant upload failure recovery** - -Add a participant upload state panel: - -```html - -``` - -Add functions: - -```js -function participantUploadPhoto() { - prototypeState.uploadState = "analyzing"; - const panel = document.getElementById("participant-upload-state"); - panel.style.display = "block"; - panel.innerHTML = ` -

AI가 시간표를 분석하고 있어요

-

수업 블록과 고정 일정을 찾는 중입니다.

- - `; -} - -function participantUploadFailed() { - prototypeState.uploadState = "failed"; - const panel = document.getElementById("participant-upload-state"); - panel.innerHTML = ` -

시간표를 읽지 못했어요

-

이미지가 흐리거나 표 영역이 잘렸을 수 있어요.

- - - `; -} -``` - -- [ ] **Step 3: Build availability review screen** - -Use the existing `#s-confirm` as the participant review screen. Set its heading and controls: - -```html -

가능 시간 확인

-

제출 전에 가능한 시간을 확인하고 수정할 수 있어요.

-
- - - -
- -``` - -Add: - -```js -function participantManualInput() { - prototypeState.availabilityState = "edited"; - go("s-confirm"); -} - -function participantConnectCalendar() { - prototypeState.calendarState = "connected"; - prototypeState.availabilityState = "detected"; - showToast("캘린더에서 가능한 시간이 감지됐어요"); - go("s-confirm"); -} - -function toggleAvailabilityCell(button) { - const pressed = button.getAttribute("aria-pressed") === "true"; - button.setAttribute("aria-pressed", String(!pressed)); - button.classList.toggle("on", !pressed); -} -``` - -- [ ] **Step 4: Add participant pre-submit soft save prompt** - -Add: - -```js -function showParticipantSubmitSavePrompt() { - prototypeState.authSavePrompt = "soft"; - const modal = document.getElementById("soft-save-modal"); - modal.querySelector("[data-save-title]").textContent = "계정 없이도 제출할 수 있어요"; - modal.querySelector("[data-save-body]").textContent = "저장하면 다음 초대에서 캘린더를 다시 연동하지 않아도 됩니다."; - modal.querySelector("[data-save-primary]").textContent = "저장하고 제출"; - modal.querySelector("[data-save-secondary]").textContent = "그냥 제출"; - modal.querySelector("[data-save-secondary]").onclick = finishParticipantSubmit; - modal.style.display = "flex"; -} - -function finishParticipantSubmit() { - document.getElementById("soft-save-modal").style.display = "none"; - prototypeState.availabilityState = "submitted"; - prototypeState.authSavePrompt = "strong"; - go("s-result"); -} -``` - -- [ ] **Step 5: Update participant completion screen** - -In `#s-result`, use: - -```html -

가능 시간이 제출됐어요

-

주최자가 시간을 확정하면 알려드릴게요.

-
-

다음 초대에서도 자동으로 응답하려면 저장할까요?

-

캘린더 연동과 기본 정보를 저장하면 다음 모임에서는 더 빠르게 제출할 수 있어요.

- - -
-``` - -- [ ] **Step 6: Run participant flow tests** - -Run: - -```powershell -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -g "참여자는" -``` - -Expected: both participant tests PASS. - ---- - -### Task 6: Responsive Polish And Visual Test Pass - -**Files:** -- Modify: `prototype/moim.html` -- Test: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Add responsive layout rules** - -Add or update the responsive CSS near the existing layout styles: - -```css -.entry-actions, -.method-grid, -.availability-review-grid { - display: grid; - gap: 10px; -} - -.method-grid { - grid-template-columns: 1fr; -} - -.availability-review-grid { - grid-template-columns: 1fr; -} - -.modal-backdrop { - position: fixed; - inset: 0; - z-index: 200; - display: flex; - align-items: flex-end; - justify-content: center; - padding: 16px; - background: rgba(23, 23, 23, 0.24); -} - -.modal-card { - width: 100%; - max-width: 420px; -} - -.availability-cell { - min-height: 48px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: white; - font: inherit; - font-weight: 700; - color: var(--text-heading); -} - -.availability-cell.on, -.availability-cell[aria-pressed="true"] { - border-color: var(--primary-500); - background: var(--primary-100); - color: var(--primary-600); -} - -@media (min-width: 768px) { - .entry-actions { - grid-template-columns: 1fr 1fr; - } - - .method-grid { - grid-template-columns: repeat(3, 1fr); - } - - .availability-review-grid { - grid-template-columns: repeat(3, 1fr); - } - - .modal-backdrop { - align-items: center; - } -} - -@media (max-width: 520px) { - .bottom-bar { - padding-bottom: max(12px, env(safe-area-inset-bottom)); - } - - .container, - .container-wide { - padding-left: 16px; - padding-right: 16px; - } -} -``` - -- [ ] **Step 2: Ensure bottom CTAs do not hide content** - -For screens with `.bottom-bar`, ensure the main container has: - -```html -style="padding-bottom:120px" -``` - -Expected: the final input/control above the sticky CTA remains visible on mobile. - -- [ ] **Step 3: Run full prototype tests on desktop and mobile** - -Run: - -```powershell -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -npx playwright test e2e/prototype-flow.spec.ts --project="Mobile Safari" -``` - -Expected: all tests PASS in both projects. - -- [ ] **Step 4: Check console errors manually** - -Open `prototype/moim.html` in the in-app browser or Playwright and inspect console logs. - -Expected: no uncaught JavaScript errors. - ---- - -### Task 7: Manual Visual Verification And Commit - -**Files:** -- Modify: `prototype/moim.html` -- Create: `e2e/prototype-flow.spec.ts` - -- [ ] **Step 1: Capture desktop screenshots** - -Use the in-app browser or Playwright screenshots for: - -```text -home -host basic info -host calendar setup -host link created -participant invite -participant review -participant submitted -``` - -Expected: each screen clearly shows the current step, primary CTA, and optional auth-save guidance where relevant. - -- [ ] **Step 2: Capture mobile screenshots** - -Use a mobile-width viewport or Mobile Safari project for the same screens. - -Expected: no CTA overlaps content, no text is clipped, tap targets are visually large enough. - -- [ ] **Step 3: Run final verification** - -Run: - -```powershell -npm run test -npx playwright test e2e/prototype-flow.spec.ts --project=chromium -npx playwright test e2e/prototype-flow.spec.ts --project="Mobile Safari" -git status --short -``` - -Expected: - -```text -vitest tests pass -prototype Playwright tests pass -git status shows only intended files plus pre-existing .agents/docs changes -``` - -- [ ] **Step 4: Commit implementation** - -Stage only intended implementation files: - -```powershell -git add -- prototype/moim.html e2e/prototype-flow.spec.ts -git commit -m "feat: 사용자 테스트용 프로토타입 개선" -``` - -Expected: commit succeeds. Do not stage `.agents/docs/1_DOMAIN_CONTEXT.md` or `.agents/docs/2_EXECUTION_PLAN.md`. - ---- - -## Self-Review - -Spec coverage: - -- Guest-first home is covered by Task 3. -- Host first-use journey is covered by Task 4. -- Participant invite response journey is covered by Task 5. -- Soft and strong auth prompts are covered by Tasks 4 and 5. -- Main error and recovery states are covered by Tasks 4 and 5. -- Mobile and desktop responsiveness are covered by Task 6. -- Manual visual/browser verification is covered by Task 7. - -Placeholder scan: - -- No unresolved placeholder markers or unspecified future work remains in this plan. -- Each implementation step names concrete files, functions, commands, and expected results. - -Type and naming consistency: - -- The shared state object is consistently named `prototypeState`. -- Save prompt states use `hidden`, `soft`, `strong`, and `dismissed`. -- Calendar states use `none`, `connected`, `rejected`, and `skipped`. -- Upload states use `idle`, `analyzing`, `success`, and `failed`. -- Availability states use `empty`, `detected`, `edited`, and `submitted`. diff --git a/src/components/schedule/AvailabilityResult.tsx b/src/components/schedule/AvailabilityResult.tsx deleted file mode 100644 index a2b789c..0000000 --- a/src/components/schedule/AvailabilityResult.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * AvailabilityResult — 공통 가용시간 결과 표시 (스켈레톤) - * - * PRD 5.2의 "공통 가능 시간: 현재까지 완료한 사람 기준 실시간 업데이트" 컴포넌트. - * - * TODO: UI 구현 후 React Testing Library로 테스트 작성 - */ - -export default function AvailabilityResult() { - return ( -
-

- 공통 가능 시간 -

-
- {/* TODO: 가용시간 결과 UI 구현 */} -

- 모든 참여자의 데이터를 취합하여 결과를 보여줍니다. -

-
-
- ); -} diff --git a/src/components/schedule/ParticipantList.tsx b/src/components/schedule/ParticipantList.tsx deleted file mode 100644 index 3e90a51..0000000 --- a/src/components/schedule/ParticipantList.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * ParticipantList — 참여자 현황 목록 (스켈레톤) - * - * PRD 5.2의 "참여자 목록: 각 참여자별 연동 상태 (완료/대기 중)" 컴포넌트. - * - * TODO: UI 구현 후 React Testing Library로 테스트 작성 - */ - -export default function ParticipantList() { - return ( -
-

참여자 목록

-
    - {/* TODO: 참여자 목록 UI 구현 */} -
  • - 참여자를 불러오는 중... -
  • -
-
- ); -} diff --git a/src/components/schedule/TimeGrid.tsx b/src/components/schedule/TimeGrid.tsx deleted file mode 100644 index 5c973cf..0000000 --- a/src/components/schedule/TimeGrid.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * TimeGrid — When2meet 스타일 시간 선택 그리드 (스켈레톤) - * - * PRD 3.3의 "시간표에서 가능한 시간 클릭" UI 컴포넌트. - * 비연동 참여자가 수동으로 가능한 시간을 선택하는 그리드. - * - * TODO: UI 구현 후 React Testing Library로 테스트 작성 - */ - -export default function TimeGrid() { - return ( -
- {/* TODO: 시간 그리드 UI 구현 */} -

시간표

-

- 가능한 시간을 드래그하여 선택하세요. -

-
- ); -} From b965f9e827aa91d11bdad20480fe671abc094d69 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:08:58 +0900 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20ICS=20=ED=8C=8C=EC=84=9C=EC=99=80?= =?UTF-8?q?=20Supabase=20=ED=99=98=EA=B2=BD=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduling/__tests__/ics-parser.test.ts | 55 +++++++ src/lib/scheduling/ics-parser.ts | 152 +++++++++++++++--- src/lib/supabase/__tests__/supabase.test.ts | 14 ++ src/lib/supabase/client.ts | 14 +- src/lib/supabase/env.ts | 21 +++ src/lib/supabase/server.ts | 46 +++--- src/middleware.ts | 38 +++-- 7 files changed, 268 insertions(+), 72 deletions(-) create mode 100644 src/lib/scheduling/__tests__/ics-parser.test.ts create mode 100644 src/lib/supabase/env.ts diff --git a/src/lib/scheduling/__tests__/ics-parser.test.ts b/src/lib/scheduling/__tests__/ics-parser.test.ts new file mode 100644 index 0000000..cf3443a --- /dev/null +++ b/src/lib/scheduling/__tests__/ics-parser.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "vitest"; +import { parseIcsToSlots } from "../ics-parser"; + +describe("parseIcsToSlots", () => { + test("UTC VEVENT의 DTSTART/DTEND를 요일 시간 슬롯으로 변환한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART:20260504T090000Z", + "DTEND:20260504T110000Z", + "SUMMARY:팀 회의", + "END:VEVENT", + "END:VCALENDAR", + ].join("\r\n"); + + expect(parseIcsToSlots(ics)).toEqual([ + { day: "MON", startHour: 9, endHour: 11 }, + ]); + }); + + test("여러 VEVENT를 시간 순서대로 반환한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART:20260506T140000Z", + "DTEND:20260506T150000Z", + "END:VEVENT", + "BEGIN:VEVENT", + "DTSTART:20260504T100000Z", + "DTEND:20260504T120000Z", + "END:VEVENT", + "END:VCALENDAR", + ].join("\n"); + + expect(parseIcsToSlots(ics)).toEqual([ + { day: "MON", startHour: 10, endHour: 12 }, + { day: "WED", startHour: 14, endHour: 15 }, + ]); + }); + + test("종일 이벤트는 해당 요일 전체 busy 슬롯으로 변환한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART;VALUE=DATE:20260508", + "DTEND;VALUE=DATE:20260509", + "END:VEVENT", + "END:VCALENDAR", + ].join("\n"); + + expect(parseIcsToSlots(ics)).toEqual([ + { day: "FRI", startHour: 0, endHour: 24 }, + ]); + }); +}); diff --git a/src/lib/scheduling/ics-parser.ts b/src/lib/scheduling/ics-parser.ts index 012bb00..ecf230a 100644 --- a/src/lib/scheduling/ics-parser.ts +++ b/src/lib/scheduling/ics-parser.ts @@ -1,24 +1,140 @@ -/** - * .ics 파일 파서 (스켈레톤) - * - * PRD 4.5 — .ics는 캘린더 앱에서 내보내기한 표준 파일 형식. - * 네이버/다음 캘린더 등 OAuth 미지원 캘린더에서 일정을 가져올 때 사용. - * - * TODO: TDD로 구현 예정 - * 1. .ics 파일 내용을 파싱하여 VEVENT 블록 추출 - * 2. DTSTART/DTEND를 TimeSlot으로 변환 - * 3. 반복 이벤트(RRULE) 처리 - */ - import type { TimeSlot } from "@/types/schedule"; +import { sortSlots } from "./time-slot"; /** - * .ics 파일 텍스트를 파싱하여 TimeSlot 배열로 변환한다. + * .ics 파일 텍스트에서 VEVENT의 DTSTART/DTEND를 busy TimeSlot 배열로 변환한다. * - * @param icsContent - .ics 파일의 텍스트 내용 - * @returns 파싱된 TimeSlot 배열 (막혀있는 시간) + * 현재 MVP는 단일 VEVENT와 종일 이벤트를 지원한다. 반복 일정(RRULE)은 후속 + * 이슈에서 별도 확장한다. */ -export function parseIcsToSlots(_icsContent: string): TimeSlot[] { - // TODO: TDD — 테스트 먼저 작성 후 구현 - throw new Error("Not implemented yet"); +export function parseIcsToSlots(icsContent: string): TimeSlot[] { + return sortSlots( + extractEvents(icsContent) + .map(eventToSlot) + .filter((slot): slot is TimeSlot => Boolean(slot)), + ); +} + +interface IcsEvent { + start?: IcsDate; + end?: IcsDate; +} + +interface IcsDate { + date: Date; + isDateOnly: boolean; +} + +function extractEvents(icsContent: string): IcsEvent[] { + const lines = unfoldLines(icsContent); + const events: IcsEvent[] = []; + let current: IcsEvent | null = null; + + for (const line of lines) { + if (line === "BEGIN:VEVENT") { + current = {}; + continue; + } + + if (line === "END:VEVENT") { + if (current) events.push(current); + current = null; + continue; + } + + if (!current) continue; + + const separatorIndex = line.indexOf(":"); + if (separatorIndex < 0) continue; + + const key = line.slice(0, separatorIndex).split(";")[0]; + const value = line.slice(separatorIndex + 1); + + if (key === "DTSTART") current.start = parseIcsDate(value); + if (key === "DTEND") current.end = parseIcsDate(value); + } + + return events; +} + +function unfoldLines(icsContent: string): string[] { + return icsContent + .replace(/\r?\n[ \t]/g, "") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); +} + +function eventToSlot(event: IcsEvent): TimeSlot | null { + if (!event.start || !event.end) return null; + + const startHour = event.start.isDateOnly ? 0 : event.start.date.getUTCHours(); + const endHour = getEndHour(event); + + if (startHour >= endHour) return null; + + return { + day: toDayCode(event.start.date.getUTCDay()), + startHour, + endHour, + }; +} + +function getEndHour(event: Required): number { + if (event.end.isDateOnly) return 24; + + const sameUtcDay = + event.start.date.getUTCFullYear() === event.end.date.getUTCFullYear() && + event.start.date.getUTCMonth() === event.end.date.getUTCMonth() && + event.start.date.getUTCDate() === event.end.date.getUTCDate(); + + return sameUtcDay ? event.end.date.getUTCHours() : 24; +} + +function parseIcsDate(value: string): IcsDate | undefined { + if (/^\d{8}$/.test(value)) { + return { + date: new Date( + Date.UTC( + Number(value.slice(0, 4)), + Number(value.slice(4, 6)) - 1, + Number(value.slice(6, 8)), + ), + ), + isDateOnly: true, + }; + } + + const match = value.match( + /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/, + ); + if (!match) return undefined; + + const [, year, month, day, hour, minute, second] = match; + return { + date: new Date( + Date.UTC( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second), + ), + ), + isDateOnly: false, + }; +} + +function toDayCode(day: number): TimeSlot["day"] { + const days: TimeSlot["day"][] = [ + "SUN", + "MON", + "TUE", + "WED", + "THU", + "FRI", + "SAT", + ]; + return days[day]; } diff --git a/src/lib/supabase/__tests__/supabase.test.ts b/src/lib/supabase/__tests__/supabase.test.ts index bf8e25b..7ff8418 100644 --- a/src/lib/supabase/__tests__/supabase.test.ts +++ b/src/lib/supabase/__tests__/supabase.test.ts @@ -37,4 +37,18 @@ describe("Supabase 클라이언트 인스턴스 생성 검증", () => { expect(client).toBeDefined(); expect(client.auth).toBeDefined(); }); + + it("Supabase URL 환경변수가 없으면 명확한 에러를 던진다", () => { + delete process.env.NEXT_PUBLIC_SUPABASE_URL; + + expect(() => createBrowserClient()).toThrow("NEXT_PUBLIC_SUPABASE_URL"); + }); + + it("Supabase anon key 환경변수가 없으면 명확한 에러를 던진다", () => { + delete process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + expect(() => createBrowserClient()).toThrow( + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + ); + }); }); diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index 18f3f96..cd25fa7 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,17 +1,13 @@ /** - * Supabase 클라이언트 (스켈레톤) + * Supabase 브라우저 클라이언트. * - * 브라우저(클라이언트 사이드)에서 Supabase에 접근하는 클라이언트. - * React 컴포넌트에서 사용. - * - * TODO: Supabase URL + anon key 연결 후 구현 + * React 클라이언트 컴포넌트에서 현재 사용자 세션과 Supabase API에 접근할 때 사용한다. */ import { createBrowserClient } from "@supabase/ssr"; +import { getSupabaseConfig } from "./env"; export function createClient() { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ); + const { url, anonKey } = getSupabaseConfig(); + return createBrowserClient(url, anonKey); } diff --git a/src/lib/supabase/env.ts b/src/lib/supabase/env.ts new file mode 100644 index 0000000..05e6f4f --- /dev/null +++ b/src/lib/supabase/env.ts @@ -0,0 +1,21 @@ +export interface SupabaseConfig { + url: string; + anonKey: string; +} + +export function getSupabaseConfig(): SupabaseConfig { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!url) { + throw new Error("NEXT_PUBLIC_SUPABASE_URL 환경변수가 설정되지 않았습니다."); + } + + if (!anonKey) { + throw new Error( + "NEXT_PUBLIC_SUPABASE_ANON_KEY 환경변수가 설정되지 않았습니다.", + ); + } + + return { url, anonKey }; +} diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index 7d94162..312faf3 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,38 +1,34 @@ /** - * Supabase 서버 클라이언트 (스켈레톤) + * Supabase 서버 클라이언트. * - * 서버 사이드(Next.js Server Components, API Routes)에서 - * Supabase에 접근하는 클라이언트. - * - * TODO: Supabase URL + service role key 연결 후 구현 + * Server Components와 API Routes에서 쿠키 기반 사용자 세션을 유지하며 Supabase에 + * 접근할 때 사용한다. */ import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; +import { getSupabaseConfig } from "./env"; export async function createClient() { const cookieStore = cookies(); + const { url, anonKey } = getSupabaseConfig(); - return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return cookieStore.getAll(); - }, - setAll(cookiesToSet) { - try { - cookiesToSet.forEach(({ name, value, options }) => - cookieStore.set(name, value, options), - ); - } catch { - // The `setAll` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - }, + return createServerClient(url, anonKey, { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options), + ); + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } }, }, - ); + }); } diff --git a/src/middleware.ts b/src/middleware.ts index 9a9b64d..a7b68ed 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,33 +1,31 @@ import { createServerClient } from "@supabase/ssr"; import { NextResponse, type NextRequest } from "next/server"; +import { getSupabaseConfig } from "@/lib/supabase/env"; export async function middleware(request: NextRequest) { + const { url, anonKey } = getSupabaseConfig(); let supabaseResponse = NextResponse.next({ request, }); - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return request.cookies.getAll(); - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value }) => - request.cookies.set(name, value), - ); - supabaseResponse = NextResponse.next({ - request, - }); - cookiesToSet.forEach(({ name, value, options }) => - supabaseResponse.cookies.set(name, value, options), - ); - }, + const supabase = createServerClient(url, anonKey, { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value), + ); + supabaseResponse = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ); }, }, - ); + }); // This will refresh session if expired await supabase.auth.getUser(); From b6f3c8cb0d25b0d5df84056639fbbc4796798467 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:16:07 +0900 Subject: [PATCH 06/14] =?UTF-8?q?chore:=20package-lock=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 164 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 132 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bfdf19..12de9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,6 +139,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -999,6 +1000,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1047,10 +1049,18 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT", + "peer": true + }, "node_modules/@dotenvx/dotenvx": { "version": "1.61.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.61.0.tgz", @@ -1900,6 +1910,7 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2038,8 +2049,9 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.59.1" }, @@ -2336,29 +2348,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", @@ -2585,6 +2574,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.106.1", "@supabase/functions-js": "2.106.1", @@ -2612,6 +2602,27 @@ "tslib": "^2.4.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -2739,6 +2750,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -2782,8 +2800,9 @@ "version": "20.19.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2792,15 +2811,16 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2812,6 +2832,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2873,6 +2894,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -3544,6 +3566,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4059,6 +4082,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4615,6 +4639,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4705,7 +4730,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4792,6 +4817,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.3.0.tgz", + "integrity": "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4987,6 +5023,13 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -5338,6 +5381,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5507,6 +5551,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5896,6 +5941,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6748,6 +6794,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -7660,6 +7707,7 @@ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", @@ -8497,6 +8545,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -9598,7 +9656,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.59.1" @@ -9617,7 +9675,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9655,6 +9713,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9871,6 +9930,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -9892,6 +9979,7 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -10047,6 +10135,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10059,6 +10148,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10067,6 +10157,13 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -11774,8 +11871,9 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11817,7 +11915,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -11973,6 +12071,7 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -12632,6 +12731,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 1fab55c0e1601618a40c719ec49ad38f2ceacfc5 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:16:48 +0900 Subject: [PATCH 07/14] =?UTF-8?q?chore:=20CI=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 245a44d..aed83ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,16 @@ on: jobs: code_quality: runs-on: ubuntu-latest + env: + NEXT_PUBLIC_BASE_URL: http://localhost:3000 + NEXT_PUBLIC_SUPABASE_URL: https://example-project.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY: test-anon-key + JWT_SECRET: test-jwt-secret-for-ci-only + ENCRYPTION_SECRET: 0000000000000000000000000000000000000000000000000000000000000000 + KAKAO_REST_API_KEY: test-kakao-key + KAKAO_REDIRECT_URI: http://localhost:3000/api/auth/kakao/callback + GOOGLE_CLIENT_ID: test-google-client-id + GOOGLE_CLIENT_SECRET: test-google-client-secret steps: - name: 저장소 체크아웃 (Checkout Repository) From 9a2168c8d51ae5c84e85b834049c813d2a9056d6 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:20:17 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20ICS=20=ED=8C=8C=EC=84=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A2=81=ED=9E=88=EA=B8=B0=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/scheduling/ics-parser.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/scheduling/ics-parser.ts b/src/lib/scheduling/ics-parser.ts index ecf230a..871b143 100644 --- a/src/lib/scheduling/ics-parser.ts +++ b/src/lib/scheduling/ics-parser.ts @@ -68,27 +68,28 @@ function unfoldLines(icsContent: string): string[] { function eventToSlot(event: IcsEvent): TimeSlot | null { if (!event.start || !event.end) return null; - const startHour = event.start.isDateOnly ? 0 : event.start.date.getUTCHours(); - const endHour = getEndHour(event); + const { start, end } = event; + const startHour = start.isDateOnly ? 0 : start.date.getUTCHours(); + const endHour = getEndHour(start, end); if (startHour >= endHour) return null; return { - day: toDayCode(event.start.date.getUTCDay()), + day: toDayCode(start.date.getUTCDay()), startHour, endHour, }; } -function getEndHour(event: Required): number { - if (event.end.isDateOnly) return 24; +function getEndHour(start: IcsDate, end: IcsDate): number { + if (end.isDateOnly) return 24; const sameUtcDay = - event.start.date.getUTCFullYear() === event.end.date.getUTCFullYear() && - event.start.date.getUTCMonth() === event.end.date.getUTCMonth() && - event.start.date.getUTCDate() === event.end.date.getUTCDate(); + start.date.getUTCFullYear() === end.date.getUTCFullYear() && + start.date.getUTCMonth() === end.date.getUTCMonth() && + start.date.getUTCDate() === end.date.getUTCDate(); - return sameUtcDay ? event.end.date.getUTCHours() : 24; + return sameUtcDay ? end.date.getUTCHours() : 24; } function parseIcsDate(value: string): IcsDate | undefined { From 99a7eb092559eefc7d4ebfecb3eeb57eb8079ac6 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:24:59 +0900 Subject: [PATCH 09/14] =?UTF-8?q?docs:=20=EC=A0=9C=ED=92=88=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EB=B2=84=EC=A0=84=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/{ => v1}/codex-work-context.md | 4 +- docs/{ => v1}/provided-documents-summary.md | 4 +- docs/{ => v1}/user-flow.md | 0 docs/v2/monetization-strategy.md | 76 +++++++++++++++++ docs/v2/prd.md | 88 ++++++++++++++++++++ docs/v2/user-flow.md | 91 +++++++++++++++++++++ 6 files changed, 259 insertions(+), 4 deletions(-) rename docs/{ => v1}/codex-work-context.md (97%) rename docs/{ => v1}/provided-documents-summary.md (97%) rename docs/{ => v1}/user-flow.md (100%) create mode 100644 docs/v2/monetization-strategy.md create mode 100644 docs/v2/prd.md create mode 100644 docs/v2/user-flow.md diff --git a/docs/codex-work-context.md b/docs/v1/codex-work-context.md similarity index 97% rename from docs/codex-work-context.md rename to docs/v1/codex-work-context.md index d35f885..6df82ee 100644 --- a/docs/codex-work-context.md +++ b/docs/v1/codex-work-context.md @@ -6,7 +6,7 @@ ## 먼저 읽을 파일 -1. `docs/user-flow.md` +1. `docs/v1/user-flow.md` - 실제 제품 흐름의 기준 문서다. 핵심 방향은 게스트 퍼스트, 초대 링크 문맥 우선이다. 2. `convention.md` - 커밋, 브랜치, 이슈, PR 규칙의 기준이다. @@ -121,7 +121,7 @@ Google/iCloud/수동 입력/ICS는 provider raw data를 표준 시간 표현으 ```text C:\Users\kksu1\Dev\MOIM에서 작업해줘. -먼저 docs/codex-work-context.md와 convention.md를 읽고 현재 개발 현황과 협업 규칙을 파악해. +먼저 docs/v1/codex-work-context.md와 convention.md를 읽고 현재 개발 현황과 협업 규칙을 파악해. 기존 작업트리에 수정/미추적 파일이 있으면 함부로 되돌리지 말고, git status와 GitHub PR 상태를 다시 확인해. 변경 후에는 npm run lint, npm run test, npm run build, 가능하면 npm run test:e2e -- --project=chromium으로 검증해. ``` diff --git a/docs/provided-documents-summary.md b/docs/v1/provided-documents-summary.md similarity index 97% rename from docs/provided-documents-summary.md rename to docs/v1/provided-documents-summary.md index cb64497..807bac1 100644 --- a/docs/provided-documents-summary.md +++ b/docs/v1/provided-documents-summary.md @@ -28,8 +28,8 @@ | 도메인 컨텍스트 | `.agents/docs/1_DOMAIN_CONTEXT.md` | 에이전트용 프로젝트 맥락 | 수정 중 | | 실행 계획 | `.agents/docs/2_EXECUTION_PLAN.md` | Phase별 구현 로드맵 | 수정 중 | | 파일 인덱스 | `.agents/docs/3_FILE_INDEX.md` | 파일 역할 지도 | 원본 MD 존재 | -| 제품 사용자 흐름 문서 | `docs/user-flow.md` | 게스트 퍼스트 실제 제품 흐름 | 원본 MD 존재 | -| 개발 현황 인수인계 | `docs/codex-work-context.md` | 현재 개발 상태와 검증 기준 | 원본 MD 존재 | +| 제품 사용자 흐름 문서 | `docs/v1/user-flow.md` | 게스트 퍼스트 실제 제품 흐름 | 원본 MD 존재 | +| 개발 현황 인수인계 | `docs/v1/codex-work-context.md` | 현재 개발 상태와 검증 기준 | 원본 MD 존재 | | AI calendar assistant PRD 리뷰 맥락 | 현재 원본 파일 미확인 | 제품/수익화/범위 리뷰 | 메모리 기반 맥락만 확인 | ## 사업계획서 핵심 내용 diff --git a/docs/user-flow.md b/docs/v1/user-flow.md similarity index 100% rename from docs/user-flow.md rename to docs/v1/user-flow.md diff --git a/docs/v2/monetization-strategy.md b/docs/v2/monetization-strategy.md new file mode 100644 index 0000000..e5b149d --- /dev/null +++ b/docs/v2/monetization-strategy.md @@ -0,0 +1,76 @@ +# MOIM 비즈니스 모델(BM) 고도화 및 수익화 전략 보고서 + +작성일: 2026-05-28 +대상: MOIM 프로젝트 팀, 지도교수 면담 및 지원사업 평가위원 제출용 +상태: 최신본 (v2.0.0) + +--- + +## 1. 비즈니스 개요 (Business Overview) +MOIM은 대학생 및 협업 단체의 최대 골칫거리인 '일정 조율 시간'을 극적으로 단축시키는 스마트 스케줄러입니다. 기존 일정 조율 툴(When2meet 등)의 고질적 한계였던 **"수익성 부재(단순 트래픽 전용 서비스)"**를 극복하기 위해, 본 전략은 대학생 특화 **광고(Ad-Tech)**와 조직용 **스몰 SaaS(SaaS)** 요금제를 유기적으로 결합한 다각적 BM 구조를 제안합니다. + +--- + +## 2. 타겟 시장 분석 (TAM - SAM - SOM) + +``` +[TAM: B2B 기업용 일정 조율 솔루션 시장] + └ 약 5,000억 원 규모 (국내 미팅/면접 스케줄링 솔루션 대상) + [SAM: 국내 대학교 학생 기구 및 동아리/학회] + └ 약 300억 원 규모 (전국 350여 개 대학, 단체/동아리 3만 개 세그먼트) + [SOM: 대학 조별과제 및 활성 팀 유저] + └ 약 15억 원 규모 (초기 거점 대학 내 헤비 유저 및 동아리 운영진) +``` + +- **초기 진입 시장 (SOM)**: 조별과제, 학과 프로젝트, 공모전 등 매주 1회 이상 시간 조율이 필요한 전국 대학생 활성 유저층. +- **성장 타겟 시장 (SAM)**: 정기적으로 대규모(20인 이상) 인원이 모여 일정을 맞추는 대학 학생회, 동아리, 학회, 대외활동 기구. +- **확장 목표 시장 (TAM)**: 대학 창업지원단 멘토링 매칭, 기업 면접 일정 조율, 리크루팅 플랫폼의 스케줄러 라이선스 연동(B2B). + +--- + +## 3. 3단계 수익 모델 로드맵 + +### 1단계: 트래픽 기반 대학생 맞춤형 제휴 및 광고 (SOM 공략) +* **BM 설명**: 1회성 무료 모임 조율 완료 및 결과 공유 화면에 대학생 맞춤형 광고 지면을 제공합니다. +* **상세 수익 기획**: + - **스터디카페/모임 공간 예약 수수료**: 모임 확정 단계에서 "예약 가능한 주변 스터디카페 목록"을 추천하고 바로 결제할 수 있는 제휴 API 연동 (중개 수수료 7~10% 취득). + - **대학생 전용 타겟 광고 (CPC/CPM)**: 신생 공모전, 대외활동, 연합동아리 모집 공고, 인근 식음료(F&B) 상권 쿠폰 광고 집행. + - **캘린더 연동 제한 (Free-tier)**: 게스트는 1개의 외부 캘린더만 연동 가능하게 하여 다중 캘린더 연동 니즈가 높은 파워 유저의 Premium 가입 유도. + +### 2단계: 동아리 및 소모임 특화 스몰 SaaS 구독 (SAM 공략) +* **BM 설명**: 20인 이상 단체를 이끄는 운영진(학생회장, 동아리 회장 등)의 관리 공수를 90% 이상 감축시켜 주는 **'MOIM Space' 정기 구독제**를 도입합니다. +* **상세 수익 기획**: + - **Space Plus 요금제**: 멤버들의 에브리타임 시간표 및 개인 일정을 워크스페이스에 상시 동기화해 두고, 정기 모임 시간을 1초 만에 자동 계산. (월 9,900원 구독제). + - **알림톡 발송 패키지**: 일정 확정 시 카카오 알림톡 자동 전송(기본 제공량 초과 시 건당 과금). + - **미응답 참여 독촉 알림**: 1클릭으로 미응답 부원에게 카카오톡 리마인더 메시지 발송 기능 제공. + +### 3단계: 기업 및 기관 대상 면접/멘토링 조율 솔루션 (TAM 공략) +* **BM 설명**: 1대N(예: 면접관 대 지원자), N대M 매칭 일정을 자동으로 수립해야 하는 기관 전용 B2B API/솔루션 판매. +* **상세 수익 기획**: + - **대학 창업지원사업/행정실 라이선스**: 창업 지원 멘토링 매칭 시스템 연동 라이선스 제공. + - **기업 면접 조율 SaaS**: 다수의 지원자 일정을 인사 담당자가 엑셀로 취합하는 대신 지원 링크 발급으로 자동 조율하는 리크루팅 연동 모듈 패키지 판매. + +--- + +## 4. 요금제 및 기능 정의 (Pricing Plan) + +| 기능 분류 | Free (일반 게스트) | Premium (개인형) | Space Plus (동아리/단체) | Enterprise (B2B/기관) | +|---|---|---|---|---| +| **이용 요금** | **무료 (₩0)** | **월 ₩2,900** | **월 ₩9,900** (대표 결제) | **별도 문의 (SaaS)** | +| **광고 여부** | 광고 노출 | 광고 제거 (Ad-Free) | 광고 제거 (Ad-Free) | 광고 제거 (Ad-Free) | +| **모임 조율 횟수** | 무제한 | 무제한 | 무제한 | 무제한 | +| **캘린더 연동** | 최대 1개 연동 | 다중 캘린더 무제한 | 다중 캘린더 무제한 | 기업 ERP/LMS 연동 | +| **에타 시간표 스캔** | 이미지 단발성 스캔 | 스캔 결과 저장/유지 | 스페이스 멤버 상시 동기화 | - | +| **워크스페이스** | 제공 안 함 | 제공 안 함 | 제공 (멤버 최대 30인) | 부서별 무제한 공간 | +| **기타 부가 기능** | 기본 공유 카드 | 자주 쓰는 시간 템플릿 | 카카오 알림톡 50건 무료 | API 연동, 보안 감사 | + +--- + +## 5. 교수님 면담용 학술/사업 논리 포인트 (Academic & Pitching Point) + +1. **"가치 중심적 온보딩의 사업적 타당성"**: + - 일반적인 창업 아이템들이 회원가입 단계에서 60% 이상 이탈하는 현상을 언급합니다. MOIM은 **'선(先) 가치 경험(조율 완료), 후(後) 관계 형성(계정 저장 및 구독)'**의 퍼널을 취함으로써 고객 획득 비용(CAC)을 낮추고 오가닉 바이럴(카카오톡 전파)을 극대화함을 설명합니다. +2. **"오픈소스 기반 스케일업 전략"**: + - 오픈소스 실전 창업 수업의 취지에 맞춰 핵심 매칭 알고리즘(`src/lib/scheduling`)과 UI 원천 컴포넌트는 오픈소스로 기여하되, **멀티 테넌트(Multi-tenant) 워크스페이스 관리, 카카오톡 알림 시스템 연동, 기업용 보안 정책** 등은 전용 SaaS 서버를 통해 소스 비공개 상용화 영역으로 분리하여 비즈니스 지속성을 담보합니다. +3. **"손익분기점(BEP) 조기 달성 모델"**: + - Supabase 및 Vercel Serverless 구조를 활용해 물리 인프라 고정비를 0원에 수렴하게 설계하고, 월 정기 구독자(Space Plus) 200개 단체만 확보해도 서버 유지비 및 API 이용료를 충당하고 BEP를 돌파하는 고효율 구조임을 입증합니다. diff --git a/docs/v2/prd.md b/docs/v2/prd.md new file mode 100644 index 0000000..1350de1 --- /dev/null +++ b/docs/v2/prd.md @@ -0,0 +1,88 @@ +# 제품 요구사항 정의서 (PRD) - v2.0.0 (수익화 및 스케일업 반영) + +작성일: 2026-05-28 +대상: MOIM 개발팀, 지도교수 면담 및 사업계획서 제출용 +상태: Draft + +--- + +## 1. 제품 개요 (Product Overview) +**MOIM**은 대학생 및 소규모 협업 팀의 조별과제, 동아리, 학회 등 반복적인 일정 조율 문제를 단 한 번의 캘린더/에브리타임 시간표 연동으로 해결하는 **대학생 특화 스마트 일정 조율 SaaS**입니다. + +### 1.1 해결하고자 하는 문제 (Problem Statement) +1. **수동 비교 피로**: 카카오톡 대화방에서 시간표 캡처 이미지를 주고받으며 수동으로 빈 시간을 대조하는 비효율. +2. **이원화된 스케줄**: 대학생의 고정 일정(수업)은 에브리타임에, 개인 일정은 캘린더(구글, 애플 등)에 흩어져 있어 단일 서비스로 빈 시간 파악 불가. +3. **높은 진입 장벽**: 기존 협업 도구들은 최초 가입 단계와 캘린더 연동 권한 요청이 길어 참여율이 저조함. +4. **수익 모델 부재**: 기존 일정 조율 툴(When2meet 등)은 트래픽은 많으나 비즈니스 모델(BM)이 없어 지속 가능한 서비스 운영이 불가능함. + +### 1.2 핵심 가치 제안 (Value Proposition) +- **Guest-First & No-Sign-Up**: 초대받은 게스트는 가입 없이 30초 내에 빈 시간 응답을 완료할 수 있습니다. +- **통합 일정 추출**: 에브리타임 시간표 이미지 스캔과 외부 캘린더 API를 단일 스케줄러로 동기화합니다. +- **지속 가능한 SaaS 구조**: 트래픽 기반 로컬/대학생 광고(Ad)와 소형 조직 대상 정기 구독(SaaS) 모델의 결합. + +--- + +## 2. 사용자 여정 및 라우팅 명세 (User Flow) +사용자의 진입 경로에 따라 차별화된 온보딩 흐름을 보장합니다. + +``` +[진입 경로] + ├─ 일반 홈 (/) ──> 서비스 소개 및 게스트 모임 생성 (/schedule/create) + ├─ 초대 링크 (/schedule/[id]) ──> 게스트 참여자 시간 선택 ──> 제출 완료 ──> [BM 넛지] + └─ 재방문 (로그인) ──> 대시보드 및 단체 워크스페이스 (/workspace) ──> [BM 과금 유도] +``` + +--- + +## 3. 핵심 기능 요구사항 (Feature Specifications) + +### 3.1 모임 생성 및 공유 (호스트) +- **스펙**: 호스트는 모임의 제목, 예상 소요 시간, 후보 날짜 범위(최대 14일)를 정해 링크를 생성합니다. +- **수익화 반영**: 무료 생성 시 최종 완료 화면에 대학생 타겟 맞춤형 프로모션 배너가 노출됩니다. + +### 3.2 캘린더 및 시간표 연동 (참여자) +- **그리드 직접 입력**: 캘린더 비연동 사용자가 드래그로 가용 시간을 기입할 수 있는 인터페이스(When2meet 스타일). +- **에브리타임 이미지 파싱**: 업로드된 시간표 이미지 내 요일/시간 격자 컬러를 로컬 픽셀 단위로 스캔하여 수업 시간을 자동으로 비가용 시간으로 추출. +- **캘린더 동기화**: Google OAuth 또는 iCloud `.ics` 파일을 통한 개인 스케줄 연동. + - **수익화 제한**: **무료 계정은 1개의 외부 캘린더만 연동 가능**. 다중 캘린더(예: 학교 계정 + 개인 계정) 연동은 Premium 등급에서만 해제. + +### 3.3 실시간 가용 시간 분석 및 확정 (AI 추천) +- **실시간 히트맵**: 참여자들이 응답을 보낼 때마다 주최자는 실시간으로 병합된 가용시간 결과를 확인합니다. +- **AI 최적 시간 추천**: 모든 참여자가 가능한 시간대 중 충돌이 가장 적고 연속된 최적의 후보 시간 3안을 시스템이 산출해 제안합니다. +- **최종 확정 및 추가**: 주최자가 시간을 최종 확정하면, 캘린더를 연동한 참여자에게 자동으로 일정이 추가되며, 비연동 참여자에게는 KakaoTalk 공유 카드와 `.ics` 다운로드 버튼을 제공합니다. + +--- + +## 4. 수익화 모델 기능 요구사항 (Monetization Features) + +### 4.1 대학생 타겟 맞춤형 광고 위젯 (Ad-Widget) +- **노출 위치**: 모임 최종 확정 공유 화면, 참여자 가용 시간 제출 완료 페이지. +- **내용**: 대학가 인근 스터디카페 할인 예약 연동, 대학생 공모전/대외활동 광고, 제휴 브랜드 프로모션 쿠폰. +- **수익화 모델**: 클릭당 단가(CPC) 및 예약 대행 수수료 모델. + +### 4.2 개인용 Premium 구독제 (구독형) +- **제공 혜택**: + - 광고 제거 (Ad-Free). + - 다중 캘린더 무제한 연동 및 자동 실시간 동기화. + - 자주 사용하는 내 고정 빈 시간(템플릿) 설정 저장. + +### 4.3 동아리/단체용 워크스페이스 'MOIM Space' (SaaS) +- **대상**: 20인 이상의 동아리 운영진, 학회, 학생회, 팀 프로젝트 빌더. +- **제공 혜택**: + - **상시 동기화**: 매번 링크를 보낼 필요 없이, 스페이스 내 부원들의 시간표/캘린더가 실시간으로 합쳐져 상시 공통 빈 시간 확인 가능. + - **알림톡 패키지**: 정기 회의 시간 확정 시 참여자들에게 카카오톡 알림톡으로 알림 자동 발송. + - **참여 독촉 시스템**: 미응답 멤버에게 1클릭으로 카카오톡 리마인더 발송. + +--- + +## 5. 비기능적 요구사항 (Non-Functional Requirements) +- **성능**: 초대 링크 접속 후 최초 렌더링(LCP)은 3G 네트워크 환경에서 1.5초 이내여야 함 (카카오톡 인앱 브라우저 이탈 방지). +- **개인정보**: 시간표 이미지 분석 시 텍스트(과목명, 교수명 등)는 서버에 저장하지 않으며 오직 '시간 블록 위치' 데이터만 추출하여 활용. +- **신뢰성**: 동시간대 100명 이상의 참여자가 동시에 히트맵 드래그 입력을 수행해도 Supabase Realtime을 통한 데이터 충돌이 없어야 함. + +--- + +## 6. 마일스톤 및 릴리즈 계획 +1. **Phase 1 (MVP 검증)**: 핵심 조율 흐름(링크 생성 -> 비로그인 응답 -> 확정) 안정화 및 로컬 스토리지 검증. +2. **Phase 2 (수익성 검증 - 베타)**: 완료 페이지 내 대학생 제휴 광고 지면 도입 및 개인 Premium 다중 캘린더 연동 해제. +3. **Phase 3 (SaaS 스케일업)**: 동아리/학생회용 'MOIM Space' 워크스페이스 및 실시간 동기화 기능 배포. diff --git a/docs/v2/user-flow.md b/docs/v2/user-flow.md new file mode 100644 index 0000000..7cf20b0 --- /dev/null +++ b/docs/v2/user-flow.md @@ -0,0 +1,91 @@ +# MOIM 사용자 흐름 문서 (v2.0.0 - 수익화 연계 개정판) + +작성일: 2026-05-28 +대상: 기획팀, 개발팀 및 지도교수 면담용 +상태: 최신본 (v2.0.0) + +--- + +## 1. 개요 (Overview) +본 문서는 MOIM의 핵심 가치인 **'게스트 퍼스트(비가입 사용)'**의 기조를 훼손하지 않으면서, 사용자가 일정 조율의 유용성을 경험하는 결정적 순간(Aha-Moment)마다 **개인 프리미엄 업그레이드** 및 **단체용 스페이스 구독**을 자연스럽게 연계하는 최신 사용자 흐름을 정의합니다. + +--- + +## 2. 전체 사용자 흐름도 (Mermaid) + +```mermaid +flowchart TD + %% 진입 단계 + A["사용자 진입"] --> B{"진입 경로"} + B -->|"초대 링크 /schedule/:id"| C["참여자 초대 화면"] + B -->|"일반 홈 /"| D["게스트 홈"] + B -->|"로그인 재방문"| E["워크스페이스 / 대시보드"] + + %% 1. 게스트 홈 흐름 + D --> F["모임 만들기 (방장)"] + F --> G["모임 정보 입력 (제목/소요시간)"] + G --> H["후보 날짜 및 시간 범위 지정"] + H --> I["초대 링크 생성 완료"] + I --> J["카카오톡 등으로 링크 공유"] + J --> K["응답 현황판 대기"] + I -->|"[BM 넛지 1]"| L["'다중 캘린더 관리를 원하면 가입하기' 제안 배너"] + + %% 2. 참여자 초대 흐름 + C --> M["모임 요약 및 직접 입력 그리드"] + M --> N{"입력 방식 선택"} + N -->|"직접 드래그 입력"| O["시간 선택 완료"] + N -->|"외부 캘린더 연동"| P{"캘린더 연동 수"} + P -->|"1개 연동 (무료)"| Q["구글/iCloud 일정 가져오기"] + P -->|"> 1개 연동 시도 (Premium)"| R["[BM 과금 넛지 2] 다중 캘린더 연동을 위해 프리미엄 가입 유도"] + N -->|"에브리타임 스캔"| S["시간표 캡처 이미지 업로드 및 픽셀 분석"] + + Q & R & S & O --> T["최종 가용 시간 제출"] + T --> U["제출 완료 페이지"] + U -->|"[BM 넛지 3]"| V["'이 멤버들과 다음에도 일정을 맞추려면? 스페이스 생성' 배너"] + + %% 3. 호스트 일정 확정 흐름 + K --> W["참여자 실시간 현황 모니터링"] + W --> X["AI 추천 시간안 (1~3안) 확인"] + X --> Y["주최자 최종 시간 확정"] + Y --> Z["확정 일정 공유 페이지"] + Z -->|"[BM 넛지 4]"| AA["하단 대학생 제휴 프로모션/스터디카페 예약 연동 광고 배너"] + Z --> AB["참여자 캘린더에 일정 자동 추가"] +``` + +--- + +## 3. 핵심 비즈니스 모델(BM) 연동 지점 상세 흐름 + +### 3.1 [BM 넛지 1] 모임 방장 완료 화면 (계정 생성 유도) +- **목적**: 1회성 호스트에게 다음 모임도 빠르게 진행할 수 있는 가치를 어필하여 회원가입 유도. +- **사용자 동선**: + 1. 모임 제목 및 범위 지정 후 `초대 링크 생성` 클릭. + 2. 링크 복사 팝업 하단에 **"현재 모임을 저장하고, 부원들의 답변이 오면 실시간 메일로 알림을 받으시겠어요?"** 문구와 함께 3초 간편 소셜 로그인 버튼 노출. + 3. 로그인 시 자동으로 `/workspace` 대시보드에 해당 모임이 귀속됨. + +### 3.2 [BM 넛지 2] 다중 캘린더 연동 시도 (개인 프리미엄 업그레이드) +- **목적**: 여러 개의 개인 일정이 얽혀 있는 대학생 파워 유저 및 직장인 타겟의 월 구독형 Premium 전환. +- **사용자 동선**: + 1. 참여자가 `외부 캘린더 연동` 버튼을 누르고 이미 구글 캘린더가 연동된 상태에서 추가로 iCloud 캘린더를 추가하려 함. + 2. 시스템은 **"두 개 이상의 캘린더를 실시간 중복 분석하여 완벽하게 빈 시간을 찾으려면 Premium으로 업그레이드 하세요 (첫 달 900원)"** 모달 팝업 노출. + 3. 가입 거부 시 기존의 1개 캘린더 연동 상태 유지. + +### 3.3 [BM 넛지 3] 참여자 가용 시간 제출 완료 페이지 (동아리/단체 SaaS 유도) +- **목적**: 주기적으로 모임을 조율하는 모임 리더들에게 **'정기 구독 워크스페이스'**를 홍보하여 B2B/B2C SaaS 전환율 극대화. +- **사용자 동선**: + 1. 참여자가 가능 시간 제출 완료. + 2. 완료 메시지 하단에 **"매주 조별과제/동아리 일정을 잡는 데 지치셨나요? 20명 부원들의 시간표를 모아 상시 빈 시간을 조회해보세요"** 카드 배너 노출. + 3. 클릭 시 동아리용 `Space Plus 요금제 (월 9,900원)` 소개 페이지 및 14일 무료 체험 신청 페이지로 연결. + +### 3.4 [BM 넛지 4] 최종 일정 확정 및 공유 카드 (제휴 타겟 광고) +- **목적**: 모임이 마침내 매칭된 순간에 공간 예약 및 F&B 제휴 결제 수수료 유치. +- **사용자 동선**: + 1. 호스트가 추천 시간 중 하나를 클릭해 `일정 확정` 완료. + 2. 참여자들에게 공유되는 확정 안내 카드 하단에 **"모임 장소가 필요하신가요? 모임 장소 근처의 제휴 스터디룸 2시간 할인 예약하기"** 광고 위젯 배치. + 3. 사용자가 스터디룸 예약 시 MOIM이 10% 예약 대행 수수료를 취득하고, 예약 결과는 확정된 모임 일정 세부정보에 자동 추가됨. + +--- + +## 4. UI/UX 디자인 가이드라인 (Aesthetics) +- **비간섭적 설계**: 과금 유도 모달 및 광고 지면은 모임 시간 조율이라는 사용자의 본원적 목적을 가로막지 않도록 부드러운 토스트 슬라이드 또는 하단 앵커드 배너 형태로 디자인합니다. +- **일러스트/애니메이션**: 요금제 전환 및 Premium 안내 시 HSL 그라데이션 광원 효과(Glassmorphism) 및 부드러운 마이크로 트랜지션 애니메이션을 적용해 프리미엄 가치를 시각적으로 느낄 수 있도록 설계합니다. From 4e20499f1ca5baecbd9d03cea30fc57dd50700a2 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:31:55 +0900 Subject: [PATCH 10/14] =?UTF-8?q?chore:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88=20=ED=8C=A8=EC=B9=98=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 107 +++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12de9ea..0dd3de8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1946,9 +1946,9 @@ } }, "node_modules/@nodable/entities": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-1.1.0.tgz", - "integrity": "sha512-bidpxmTBP0pOsxULw6XlxzQpTgrAGLDHGBK/JuWhPDL6ZV0GZ/PmN9CA9do6e+A9lYI6qx6ikJUtJYRxup141g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", "funding": [ { "type": "github", @@ -2713,9 +2713,9 @@ } }, "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3049,9 +3049,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -5981,12 +5981,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -6047,9 +6047,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -6063,9 +6063,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -6074,13 +6074,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.6.0.tgz", - "integrity": "sha512-5G+uaEBbOm9M4dgMOV3K/rBzfUNGqGqoUTaYJM3hBwM8t71w07gxLQZoTsjkY8FtfjabqgQHEkeIySBDYeBmJw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", "funding": [ { "type": "github", @@ -6089,10 +6090,11 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^1.1.0", - "fast-xml-builder": "^1.1.4", + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6790,9 +6792,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", - "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", "peer": true, "engines": { @@ -7004,9 +7006,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -8843,9 +8845,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -9695,9 +9697,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -9715,7 +9717,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10072,9 +10074,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -11328,9 +11330,9 @@ } }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -12580,6 +12582,21 @@ "node": ">=18" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", From 89205915aaae6f102b9a361630befc6f7c78bc8b Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:53:35 +0900 Subject: [PATCH 11/14] =?UTF-8?q?fix:=20CI=20clean=20install=20=EC=9E=AC?= =?UTF-8?q?=ED=98=84=EC=84=B1=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 1 + package-lock.json | 61 ++++++++++++++++++++++------------------ package.json | 3 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed83ab..24527ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: KAKAO_REDIRECT_URI: http://localhost:3000/api/auth/kakao/callback GOOGLE_CLIENT_ID: test-google-client-id GOOGLE_CLIENT_SECRET: test-google-client-secret + DATABASE_URL: file:./ci.db steps: - name: 저장소 체크아웃 (Checkout Repository) diff --git a/package-lock.json b/package-lock.json index 0dd3de8..4d49280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "moim-app", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@base-ui/react": "^1.4.0", "@prisma/client": "^5.22.0", @@ -139,7 +140,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1000,7 +1000,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1049,7 +1048,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1910,7 +1908,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2051,7 +2048,6 @@ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.59.1" }, @@ -2348,6 +2344,29 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", @@ -2574,7 +2593,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.106.1", "@supabase/functions-js": "2.106.1", @@ -2755,7 +2773,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/bcryptjs": { "version": "2.4.6", @@ -2802,7 +2821,6 @@ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2820,7 +2838,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2832,7 +2849,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2894,7 +2910,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -3566,7 +3581,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4082,7 +4096,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4639,7 +4652,6 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -5028,7 +5040,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -5381,7 +5394,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5551,7 +5563,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5941,7 +5952,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6796,7 +6806,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -7709,7 +7718,6 @@ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", @@ -8553,6 +8561,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9715,7 +9724,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -9938,6 +9946,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9953,6 +9962,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9981,7 +9991,6 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -10137,7 +10146,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10150,7 +10158,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10164,7 +10171,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/read-cache": { "version": "1.0.0", @@ -11875,7 +11883,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12073,7 +12080,6 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -12748,7 +12754,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index efecc73..23fedca 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", - "prepare": "husky" + "prepare": "husky", + "postinstall": "prisma generate" }, "dependencies": { "@base-ui/react": "^1.4.0", From 194849b398851b8da116e991bb4426bbd36d7f6b Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 03:56:46 +0900 Subject: [PATCH 12/14] =?UTF-8?q?chore:=20GitHub=20Actions=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20=EA=B2=BD=EA=B3=A0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24527ad..27538e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,10 @@ jobs: steps: - name: 저장소 체크아웃 (Checkout Repository) - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Node.js 세팅 (Setup Node.js) - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' From e632bc6e535983b1915a3827482ce46b8f510597 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 10:36:18 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20=EB=A0=88=ED=8F=AC=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=EC=84=B1=EA=B3=BC=20=EB=AC=B8=EC=84=9C=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 19 +- .github/workflows/issue-compliance.yml | 3 + .github/workflows/pr-compliance.yml | 11 +- ARCHITECTURE.md | 74 +- README.md | 39 + convention.md | 88 +- docs/README.md | 25 + docs/v1/codex-work-context.md | 24 +- docs/v1/provided-documents-summary.md | 24 +- package-lock.json | 1854 +++++++++++------ package.json | 31 +- playwright.config.ts | 9 +- .../migration.sql | 25 + prisma/schema.prisma | 24 + scripts/ensure-sqlite-schema.mjs | 83 + scripts/with-database-url.mjs | 46 + .../api/schedules/[id]/availability/route.ts | 11 +- src/app/api/schedules/[id]/route.ts | 12 +- src/app/api/schedules/route.ts | 9 +- src/app/schedule/[id]/page.tsx | 16 +- src/lib/prisma.ts | 4 + ...{in-memory-store.test.ts => store.test.ts} | 75 +- .../{in-memory-store.ts => store.ts} | 205 +- .../scheduling/__tests__/ics-parser.test.ts | 64 + src/lib/scheduling/ics-parser.ts | 3 +- src/lib/supabase/__tests__/supabase.test.ts | 22 + src/lib/supabase/env.ts | 4 +- src/lib/supabase/server.ts | 2 +- tsconfig.json | 21 +- 29 files changed, 1876 insertions(+), 951 deletions(-) create mode 100644 README.md create mode 100644 docs/README.md create mode 100644 prisma/migrations/20260528001000_add_schedule_persistence/migration.sql create mode 100644 scripts/ensure-sqlite-schema.mjs create mode 100644 scripts/with-database-url.mjs rename src/lib/schedules/__tests__/{in-memory-store.test.ts => store.test.ts} (58%) rename src/lib/schedules/{in-memory-store.ts => store.ts} (51%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27538e9..380b213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI Pipeline +permissions: + contents: read + on: pull_request: branches: @@ -17,8 +20,6 @@ jobs: NEXT_PUBLIC_BASE_URL: http://localhost:3000 NEXT_PUBLIC_SUPABASE_URL: https://example-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY: test-anon-key - JWT_SECRET: test-jwt-secret-for-ci-only - ENCRYPTION_SECRET: 0000000000000000000000000000000000000000000000000000000000000000 KAKAO_REST_API_KEY: test-kakao-key KAKAO_REDIRECT_URI: http://localhost:3000/api/auth/kakao/callback GOOGLE_CLIENT_ID: test-google-client-id @@ -32,12 +33,22 @@ jobs: - name: Node.js 세팅 (Setup Node.js) uses: actions/setup-node@v6 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" - name: 패키지 설치 (Install Dependencies) run: npm ci + - name: CI 전용 보안 키 생성 (Generate CI Secrets) + run: | + { + echo "JWT_SECRET=$(openssl rand -base64 32)" + echo "ENCRYPTION_SECRET=$(openssl rand -hex 32)" + } >> "$GITHUB_ENV" + + - name: 데이터베이스 마이그레이션 (Database Migration) + run: npm run db:migrate + - name: 코드 린트 검사 (Lint Check) run: npm run lint diff --git a/.github/workflows/issue-compliance.yml b/.github/workflows/issue-compliance.yml index 46b6454..436d86b 100644 --- a/.github/workflows/issue-compliance.yml +++ b/.github/workflows/issue-compliance.yml @@ -1,5 +1,8 @@ name: 이슈 템플릿 검사 (Compliance Check) +permissions: + issues: read + on: issues: types: [opened, edited] diff --git a/.github/workflows/pr-compliance.yml b/.github/workflows/pr-compliance.yml index cc1fff5..af20276 100644 --- a/.github/workflows/pr-compliance.yml +++ b/.github/workflows/pr-compliance.yml @@ -1,5 +1,8 @@ name: PR 템플릿 검사 +permissions: + pull-requests: read + on: pull_request: types: [opened, edited, synchronize] @@ -12,6 +15,12 @@ jobs: env: PR_BODY: ${{ github.event.pull_request.body }} run: | + # 체크리스트 자체가 누락된 PR은 템플릿을 사용하지 않은 것으로 본다 + if ! echo "$PR_BODY" | grep -Eq "\- \[[xX ]\]"; then + echo "❌ PR 템플릿의 체크리스트가 누락되었습니다. 템플릿에 맞게 작성해주세요." + exit 1 + fi + # 체크 안 된 항목('- [ ]')이 하나라도 남아있으면 에러 뱉기 if echo "$PR_BODY" | grep -q "\- \[ \]"; then echo "❌ 체크리스트 중 완료되지 않은 항목이 있습니다! 모두 [x]로 체크해야 Merge할 수 있습니다." @@ -23,5 +32,5 @@ jobs: echo "❌ PR 설명이 너무 짧습니다. 템플릿에 맞게 어떤 작업을 했는지 명확하게 설명해주세요." exit 1 fi - + echo "✅ PR 템플릿 검사 통과 성공!" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 09e4fa1..2fd03bb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -7,15 +7,15 @@ ## 기술 스택 요약 -| 영역 | 기술 | 버전 | -|------|------|------| -| 프레임워크 | Next.js (App Router) | 14 | -| 언어 | TypeScript | 5.x | -| 스타일링 | Tailwind CSS + shadcn/ui | v3 | -| 단위 테스트 | Vitest + React Testing Library | - | -| E2E 테스트 | Playwright | - | -| 백엔드 | Supabase (PostgreSQL + Auth + Realtime) | - | -| 배포 | Vercel | - | +| 영역 | 기술 | 버전 | +| ----------- | --------------------------------------- | ---- | +| 프레임워크 | Next.js (App Router) | 14 | +| 언어 | TypeScript | 5.x | +| 스타일링 | Tailwind CSS + shadcn/ui | v3 | +| 단위 테스트 | Vitest + React Testing Library | - | +| E2E 테스트 | Playwright | - | +| 백엔드 | Supabase (PostgreSQL + Auth + Realtime) | - | +| 배포 | Vercel | - | --- @@ -36,11 +36,7 @@ MOIM/ │ │ └── globals.css # 전역 CSS + 디자인 토큰 │ │ │ ├── 📁 components/ # 재사용 가능한 UI 컴포넌트 -│ │ ├── 📁 ui/ # shadcn/ui 원시 컴포넌트 (Button, Card 등) -│ │ └── 📁 schedule/ # 스케줄링 도메인 전용 컴포넌트 -│ │ ├── TimeGrid.tsx # When2meet 스타일 시간 선택 그리드 -│ │ ├── ParticipantList.tsx # 참여자 현황 목록 -│ │ └── AvailabilityResult.tsx # 공통 가용시간 결과 +│ │ └── 📁 ui/ # shadcn/ui 원시 컴포넌트 (Button 등) │ │ │ ├── 📁 lib/ # 비즈니스 로직 & 유틸리티 │ │ ├── 📁 scheduling/ # ⭐ 핵심 알고리즘 (순수 함수) @@ -51,6 +47,7 @@ MOIM/ │ │ ├── 📁 supabase/ # Supabase DB 클라이언트 │ │ │ ├── client.ts # 브라우저용 클라이언트 │ │ │ └── server.ts # 서버 사이드 클라이언트 +│ │ ├── 📁 schedules/ # 일정 링크/참여 데이터 저장소 │ │ └── utils.ts # 공용 유틸리티 │ │ │ ├── 📁 types/ # TypeScript 타입 정의 @@ -66,6 +63,7 @@ MOIM/ ├── 📁 .github/ # GitHub 자동화 │ └── 📁 workflows/ # CI/CD 파이프라인 │ +├── scripts/ # 로컬 DB schema 보장 등 개발 스크립트 ├── vitest.config.ts # Vitest 설정 ├── playwright.config.ts # Playwright 설정 ├── tailwind.config.ts # Tailwind CSS 설정 @@ -81,18 +79,20 @@ MOIM/ Next.js App Router의 **파일 = URL** 규칙을 활용하여, PRD의 사용자 플로우를 그대로 폴더로 표현합니다. -| URL | 파일 | PRD 매핑 | -|-----|------|----------| -| `/login` | `(auth)/login/page.tsx` | 3.1 로그인/회원가입 | -| `/schedule/create` | `schedule/create/page.tsx` | 3.1 일정 잡기 생성 | -| `/schedule/:id` | `schedule/[id]/page.tsx` | 3.2~3.3 참여자 진입 | +| URL | 파일 | PRD 매핑 | +| ------------------ | -------------------------- | ------------------- | +| `/login` | `(auth)/login/page.tsx` | 3.1 로그인/회원가입 | +| `/schedule/create` | `schedule/create/page.tsx` | 3.1 일정 잡기 생성 | +| `/schedule/:id` | `schedule/[id]/page.tsx` | 3.2~3.3 참여자 진입 | **`(auth)` 괄호 그룹이란?** Next.js에서 폴더명을 괄호로 감싸면 URL에 영향을 주지 않고 레이아웃만 분리할 수 있습니다. + - `/login`의 URL은 그대로지만, 별도의 `layout.tsx`를 가져 GNB가 없는 레이아웃을 사용합니다. **`[id]` 동적 라우트란?** 대괄호로 감싼 폴더는 URL의 일부를 변수로 받습니다. + - `/schedule/abc123` → `params.id = 'abc123'` ### 2. `src/components/` — UI는 범용과 도메인으로 분리한다 @@ -104,7 +104,7 @@ components/ ``` - `ui/`: 버튼, 카드, 모달 등 어떤 프로젝트에서든 쓸 수 있는 범용 컴포넌트 -- `schedule/`: TimeGrid, ParticipantList 등 MOIM 서비스에만 존재하는 컴포넌트 +- 스케줄 화면처럼 한 라우트에 강하게 묶인 UI는 현재 `src/app/schedule/*` 아래에 둔다. ### 3. `src/lib/scheduling/` — 핵심 로직은 UI와 분리한다 @@ -113,11 +113,12 @@ UI 없이 독립적으로 실행되므로, 테스트가 빠르고 정확합니 ```typescript // 이렇게 React 없이 단독으로 테스트 가능 -const result = findCommonSlots([userA, userB]) -expect(result).toEqual([{ day: 'MON', startHour: 13, endHour: 15 }]) +const result = findCommonSlots([userA, userB]); +expect(result).toEqual([{ day: "MON", startHour: 13, endHour: 15 }]); ``` **왜 분리하나?** + - 순수 함수는 입력→출력만 검증하면 되므로 TDD에 최적 - 브라우저 환경(jsdom)이 필요 없어 테스트가 10배 빨라짐 - 나중에 서버 사이드에서도 같은 로직을 재사용 가능 @@ -132,6 +133,7 @@ lib/scheduling/ ``` **왜 루트 `tests/` 폴더가 아니라 코드 옆에 두는가?** + - 파일 탐색 시 구현과 테스트를 한눈에 볼 수 있음 - 새 기능 추가 시 "테스트 파일은 어디 만들지?" 고민이 없음 - AI 도구에게 "이 파일 테스트해줘"라고 할 때 컨텍스트 파악이 빠름 @@ -153,20 +155,21 @@ Python에서 Pydantic 모델 → pytest 테스트 → 비즈니스 로직 순서 ### 3계층 테스트 (Testing Trophy) -| 계층 | 도구 | 대상 | 비중 | -|------|------|------|------| -| **Unit** | Vitest | `lib/scheduling/` 순수 로직 | 50% | -| **Integration** | React Testing Library + Vitest | `components/` 상호작용 | 30% | -| **E2E** | Playwright | 사용자 전체 플로우 | 20% | +| 계층 | 도구 | 대상 | 비중 | +| --------------- | ------------------------------ | --------------------------- | ---- | +| **Unit** | Vitest | `lib/scheduling/` 순수 로직 | 50% | +| **Integration** | React Testing Library + Vitest | `components/` 상호작용 | 30% | +| **E2E** | Playwright | 사용자 전체 플로우 | 20% | ### 테스트 실행 명령어 -| 명령어 | 용도 | -|--------|------| -| `npm run test` | 모든 단위 테스트 한 번 실행 (CI용) | -| `npm run test:watch` | 파일 저장 시 자동 재실행 (개발용) | -| `npm run test:coverage` | 커버리지 리포트와 함께 실행 | -| `npm run test:e2e` | 브라우저 E2E 테스트 실행 | +| 명령어 | 용도 | +| ----------------------- | ---------------------------------- | +| `npm run test` | 모든 단위 테스트 한 번 실행 (CI용) | +| `npm run test:watch` | 파일 저장 시 자동 재실행 (개발용) | +| `npm run test:coverage` | 커버리지 리포트와 함께 실행 | +| `npm run test:e2e` | 브라우저 E2E 테스트 실행 | +| `npm run db:migrate` | 로컬 SQLite schema 준비 | ### TDD 워크플로우 @@ -186,8 +189,9 @@ PR을 올리면 아래 검사가 자동으로 실행됩니다: ``` PR 생성 → 템플릿 검사 → 린트(npm run lint) - → 테스트(npm run test) - → 빌드(npm run build) + → 테스트(npm run test) + → 빌드(npm run build) + → Chromium E2E(npm run test:e2e -- --project=chromium) → 모두 ✅ → Merge 가능 ``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3ae31d --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# MOIM + +MOIM은 주최자가 모임 링크를 만들고, 참여자가 로그인 없이 가능한 시간을 제출하면 공통 가능한 시간을 추천해 주는 일정 조율 앱입니다. + +## 빠른 시작 + +```powershell +npm ci +npm run db:migrate +npm run dev +``` + +로컬 기본 DB는 `DATABASE_URL`이 없을 때 `file:./dev.db`를 사용합니다. 실제 배포나 공유 환경에서는 `.env.example`을 기준으로 `.env`를 준비하세요. + +## 검증 명령 + +```powershell +npm run lint +npm run test +npm run test:coverage +npm run build +npm run test:e2e -- --project=chromium +``` + +`test`, `test:coverage`, `test:e2e`, `dev`는 실행 전에 로컬 SQLite schema를 보장합니다. + +## 먼저 읽을 문서 + +1. `docs/README.md`: 현재 문서 기준과 v1/v2 구분 +2. `docs/v2/user-flow.md`: 최신 사용자 흐름 및 수익화 연결안 +3. `docs/v1/codex-work-context.md`: 현재 개발 handoff와 주의점 +4. `ARCHITECTURE.md`: 코드 구조와 테스트 전략 +5. `convention.md`: 커밋, 브랜치, 이슈, PR 규칙 + +## 현재 안정성 기준 + +- CI는 lint, unit test, build, Chromium E2E를 실행합니다. +- schedule 생성/참여 API는 process memory가 아니라 SQLite-backed Prisma store를 사용합니다. +- `.env`, 로컬 DB, Playwright 결과물, 문서 제출용 바이너리는 Git에 올리지 않습니다. diff --git a/convention.md b/convention.md index 119551d..ce76799 100644 --- a/convention.md +++ b/convention.md @@ -10,22 +10,24 @@ 소프트웨어 개발에서는 수많은 커밋(저장 기록)이 쌓입니다. 나중에 문제가 생기거나 코드를 리뷰할 때 "이 코드가 왜, 무엇을 위해 추가/수정되었는지"를 쉽게 파악하려면 커밋 메시지에 규칙이 필요합니다. 우리는 전 세계적으로 가장 널리 쓰이는 `Angular Conventional Commits` 방식을 사용합니다. ### ❓ 왜 커밋 규칙(Prefix)을 앞에 달아야 할까요? + "버튼 수정함", "진짜 최종", "오류 해결" 같은 메시지는 나중에 코드 히스토리를 분석할 때 무슨 버튼인지, 무슨 오류인지 파악할 수가 없습니다. 앞에 태그(Prefix)를 붙이면 커밋 목록만 보고도 "아, 이 커밋들은 새로운 기능(feat)들이고, 이 커밋은 버그 수정(fix)이구나"를 한눈에 알아볼 수 있습니다. ### ✅ 커밋 메시지 규칙 (반드시 지켜야 할 형식) + 커밋 메시지 첫 줄은 아래와 같이 작성합니다: `[타입]: [작업 내용 요약]` 예) `feat: 구글 캘린더 연동 기능 추가` -| 타입 | 의미 / 언제 사용하나요? | 예시 | -| --- | --- | --- | -| **feat** | 새로운 기능을 추가할 때 (Feature) | `feat: 카카오 소셜 로그인 연동` | -| **fix** | 버그를 고쳤을 때 | `fix: 스케줄링 그리드 클릭 안 되는 에러 해결` | -| **design** | CSS 등 사용자 UI 디자인 변경 시 | `design: 버튼 색상을 메인 브랜드 색상으로 변경` | -| **refactor**| 결과나 기능의 변경 없이, 코드를 더 깔끔하게 다시 정리할 때 | `refactor: 로그인 함수 로직 분리 (중복 제거)` | -| **docs** | README나 주석 등 문서를 수정할 때 | `docs: README 실행 방법 업데이트` | -| **chore** | 설정 파일 변동, 패키지 추가 (기능과 무관한 잡일) | `chore: 캘린더 라이브러리 설치` | +| 타입 | 의미 / 언제 사용하나요? | 예시 | +| ------------ | ---------------------------------------------------------- | ----------------------------------------------- | +| **feat** | 새로운 기능을 추가할 때 (Feature) | `feat: 카카오 소셜 로그인 연동` | +| **fix** | 버그를 고쳤을 때 | `fix: 스케줄링 그리드 클릭 안 되는 에러 해결` | +| **design** | CSS 등 사용자 UI 디자인 변경 시 | `design: 버튼 색상을 메인 브랜드 색상으로 변경` | +| **refactor** | 결과나 기능의 변경 없이, 코드를 더 깔끔하게 다시 정리할 때 | `refactor: 로그인 함수 로직 분리 (중복 제거)` | +| **docs** | README나 주석 등 문서를 수정할 때 | `docs: README 실행 방법 업데이트` | +| **chore** | 설정 파일 변동, 패키지 추가 (기능과 무관한 잡일) | `chore: 캘린더 라이브러리 설치` | > 🚨 **주의**: 우리 프로젝트에는 커밋 검사 봇(Husky & Commitlint)이 백그라운드에 심어져 있습니다. 만약 `버튼 추가`처럼 규칙에 맞지 않는 메시지로 `git commit`을 하려 하면 **에러가 나면서 커밋 자체가 거부**됩니다. 꼭 규칙을 지켜주세요! @@ -37,21 +39,23 @@ 따라서 우리는 각자의 도화지(Branch)를 따로 파서 작업하고 완벽하게 만들어진 나뭇가지들을 하나로 합치는 **Git Flow (단순화 버전)**를 사용합니다. ### ❓ 왜 이런 브랜치 구조가 필요한가요? + - 안전성: 내가 코드를 망쳐도 내 브랜치 안에서만 고장나므로, 팀 전체 프로젝트(`main`)에는 타격을 주지 않습니다. ### ✅ 브랜치 네이밍 및 워크플로우 -1. **`main` (배포 브랜치)**: +1. **`main` (배포 브랜치)**: - 사용자가 접속하게 될 실제 운영 서버의 코드입니다. 절대 직접 코드를 쓰지 않습니다. 항상 에러 없이 완벽한 상태를 유지합니다. 2. **`dev` (개발 통합 브랜치)**: - 우리가 작업한 코드들이 우선적으로 합쳐지는 테스트 서버용 브랜치입니다. 기능들이 하나씩 완성될 때마다 여기로 합칩니다. 3. **`feature/이슈번호-기능명` (내 작업용 브랜치)**: - - 새로운 기능을 만들기 전, **GitHub에서 먼저 Issue를 생성**하여 작업 내용을 알립니다. + - 새로운 기능을 만들기 전, **GitHub에서 먼저 Issue를 생성**하여 작업 내용을 알립니다. - 발급받은 이슈 번호(예: #12)를 사용해, `dev` 브랜치에서 가지를 쳐서 내 전용 브랜치를 만듭니다. - - 예시: `feature/12-kakao-login`, `feature/15-calendar-ui` + - 예시: `feature/12-kakao-login`, `feature/15-calendar-ui` - 기능 완성이 다 되면 PR(Pull Request)을 날려 `dev`에 합칩니다. ### 👉 일반적인 작업 순서 (매우 중요) + 1. **Issue 생성**: GitHub에서 버그나 기능 추가 이슈를 만들고 이슈 번호(예: #12)를 받습니다. 2. 로컬에서 `git checkout dev` (최신 통합 개발 상태로 이동) 3. `git pull origin dev` (최신 코드 받기) @@ -67,6 +71,7 @@ GitHub에서 이슈를 만들면 자동으로 번호(예: #12)가 발급되고, 이 번호를 브랜치 이름과 PR에 연결해서 **"어떤 이슈를 해결하기 위한 코드인지"** 추적할 수 있게 됩니다. 이슈 없이 바로 코딩을 시작하면 나중에 누가 왜 이 코드를 짰는지 아무도 모르게 됩니다. ### ❓ 왜 이슈를 먼저 만들어야 할까요? + - **중복 방지**: "어? 나도 그거 만들고 있었는데?" 같은 사고를 예방합니다. - **진행 상황 추적**: 프로젝트 탭에서 누가 어떤 작업을 하고 있는지 한눈에 보입니다. - **코드와의 연결**: PR에 `Close #12`를 적으면 PR이 merge될 때 이슈가 자동으로 닫힙니다. 깔끔! @@ -94,12 +99,12 @@ GitHub에서 "New Issue"를 누르면 아래 양식이 자동으로 채워져 ``` -| 항목 | 왜 적어야 하나요? | -| --- | --- | -| 🎯 어떤 기능이 필요한가요? | 팀원이 "이 이슈가 뭘 하려는 건지" 5초 만에 파악하기 위해 | -| ✨ 구상하는 해결책 | 방향을 미리 공유해야 리뷰어가 "그건 이렇게 하는 게 낫지 않아?"라고 조언해줄 수 있음 | -| ⚖️ 대안 | 다른 방법을 고려했다면 적어두면 나중에 "왜 이 방법을 택했지?"를 기록으로 남김 | -| 📝 추가 정보 | 참고 링크, 디자이너가 준 시안 이미지 등을 여기에 첨부 | +| 항목 | 왜 적어야 하나요? | +| -------------------------- | ----------------------------------------------------------------------------------- | +| 🎯 어떤 기능이 필요한가요? | 팀원이 "이 이슈가 뭘 하려는 건지" 5초 만에 파악하기 위해 | +| ✨ 구상하는 해결책 | 방향을 미리 공유해야 리뷰어가 "그건 이렇게 하는 게 낫지 않아?"라고 조언해줄 수 있음 | +| ⚖️ 대안 | 다른 방법을 고려했다면 적어두면 나중에 "왜 이 방법을 택했지?"를 기록으로 남김 | +| 📝 추가 정보 | 참고 링크, 디자이너가 준 시안 이미지 등을 여기에 첨부 | ### 📋 이슈 템플릿 B: 버그 리포트 `[BUG]` @@ -130,13 +135,13 @@ GitHub에서 "New Issue"를 누르면 아래 양식이 자동으로 채워져 - 브라우저: Chrome 130 ``` -| 항목 | 왜 적어야 하나요? | -| --- | --- | -| 🐛 버그 설명 | "안 됩니다"만 적으면 아무도 도와줄 수 없음. **뭐가 안 되는지** 구체적으로! | -| 🔄 재현 방법 | 개발자가 같은 순서대로 따라 해서 버그를 직접 확인하려면 단계가 필요함 | -| 🎯 예상 정상 동작 | "원래 이렇게 돼야 하는데 안 된다"를 알아야 고칠 목표가 생김 | -| 📸 스크린샷/로그 | 글보다 캡처 한 장이 열 배 빠름. 특히 콘솔 에러 로그는 필수 | -| 💻 환경 정보 | "내 컴에선 되는데?" 문제를 방지. OS, 브라우저 버전이 다르면 버그가 달라짐 | +| 항목 | 왜 적어야 하나요? | +| ----------------- | -------------------------------------------------------------------------- | +| 🐛 버그 설명 | "안 됩니다"만 적으면 아무도 도와줄 수 없음. **뭐가 안 되는지** 구체적으로! | +| 🔄 재현 방법 | 개발자가 같은 순서대로 따라 해서 버그를 직접 확인하려면 단계가 필요함 | +| 🎯 예상 정상 동작 | "원래 이렇게 돼야 하는데 안 된다"를 알아야 고칠 목표가 생김 | +| 📸 스크린샷/로그 | 글보다 캡처 한 장이 열 배 빠름. 특히 콘솔 에러 로그는 필수 | +| 💻 환경 정보 | "내 컴에선 되는데?" 문제를 방지. OS, 브라우저 버전이 다르면 버그가 달라짐 | > 🚨 **시스템 강제**: 이슈 제목이 `[FEAT]` 또는 `[BUG]`로 시작하지 않거나, 본문 항목을 비워두면 GitHub Action 봇이 자동으로 경고하고 이슈를 차단합니다. @@ -147,7 +152,9 @@ GitHub에서 "New Issue"를 누르면 아래 양식이 자동으로 채워져 내가 짠 코드를 메인 코드에 합치고 싶을 때, 무작정 합치지 않고 **"내 코드 합쳐도 돼요?"** 하고 허락을 구하는 과정이 Pull Request입니다. ### ❓ 왜 PR을 통해서만 합칠까요? + 개인의 컴퓨터에서는 잘 돌아가던 코드가 모두의 코드와 섞이면 안 돌아갈 수 있습니다. PR을 통하면: + 1. **코드 리뷰**: 다른 팀원이 내 코드를 보고 실수를 캐치해줄 수 있음 2. **자동 검증(CI)**: 봇이 린트, 빌드 테스트를 대신 돌려줌 3. **기록이 남음**: 언제, 누가, 왜 이 코드를 합쳤는지 영구 기록 @@ -179,27 +186,30 @@ GitHub에서 PR을 생성하면 아래 양식이 자동으로 채워져 나옵 Close #12 ``` -| 항목 | 왜 적어야 하나요? | 작성 팁 | -| --- | --- | --- | -| 🚀 작업 내용 (What) | 리뷰어가 코드를 열기 전에 "대략 뭘 했구나" 감을 잡기 위해 | 한 줄로 핵심만. "로그인 페이지 UI 구현" 정도면 충분 | -| 📣 핵심 변경 이유 (Why) | "왜 이걸 만들었는가"를 기록해야 나중에 되돌아볼 때 이해가 됨 | PRD 어디에 나와있는지, 어떤 이슈를 해결하는지 한 줄 | -| 📸 스크린샷 | UI 변경이 있으면 리뷰어가 직접 실행하지 않아도 확인 가능 | 기능 변경이면 생략해도 됨 | -| ⚠️ 체크리스트 | 셀프 점검표. **4개 항목 모두 `[x]` 체크해야 봇 통과** | 하나라도 빠지면 ❌ | -| 🔗 관련 이슈 | `Close #12`라고 적으면 PR merge 시 해당 이슈가 **자동으로 닫힘** | 반드시 이슈 번호 기입! | +| 항목 | 왜 적어야 하나요? | 작성 팁 | +| ----------------------- | ---------------------------------------------------------------- | --------------------------------------------------- | +| 🚀 작업 내용 (What) | 리뷰어가 코드를 열기 전에 "대략 뭘 했구나" 감을 잡기 위해 | 한 줄로 핵심만. "로그인 페이지 UI 구현" 정도면 충분 | +| 📣 핵심 변경 이유 (Why) | "왜 이걸 만들었는가"를 기록해야 나중에 되돌아볼 때 이해가 됨 | PRD 어디에 나와있는지, 어떤 이슈를 해결하는지 한 줄 | +| 📸 스크린샷 | UI 변경이 있으면 리뷰어가 직접 실행하지 않아도 확인 가능 | 기능 변경이면 생략해도 됨 | +| ⚠️ 체크리스트 | 셀프 점검표. **4개 항목 모두 `[x]` 체크해야 봇 통과** | 하나라도 빠지면 ❌ | +| 🔗 관련 이슈 | `Close #12`라고 적으면 PR merge 시 해당 이슈가 **자동으로 닫힘** | 반드시 이슈 번호 기입! | > 🚨 **시스템 강제**: 체크리스트를 하나라도 체크하지 않거나(`- [ ]` 상태), PR 설명이 너무 짧으면 **CI 봇이 ❌ 실패를 뱉고 Merge 버튼이 비활성화**됩니다. 양식을 성실하게 작성해야 병합이 가능합니다. ### 🤖 CI 봇이 실행하는 검사 목록 -PR을 올리면 아래 3가지 검사가 자동으로 돌아갑니다: -| 검사 | 하는 일 | 실패하면? | -| --- | --- | --- | -| **린트 검사** (`npm run lint`) | 코드 문법 오류, 미사용 변수 등 검출 | 빨간 ❌, 코드 고쳐서 다시 push | -| **빌드 테스트** (`npm run build`) | 프로젝트가 에러 없이 빌드되는지 확인 | 빨간 ❌, 빌드 에러 수정 후 다시 push | -| **템플릿 검사** | 체크리스트 완료 여부, 본문 길이 검증 | 빨간 ❌, PR 설명을 보강해서 Edit | -| **팀원 승인 (Approve)** | 최소 1명 이상의 팀원이 코드를 읽고 ✅ Approve를 해야 함 | Merge 불가 상태 유지 | +PR을 올리면 아래 검사가 자동으로 돌아갑니다: + +| 검사 | 하는 일 | 실패하면? | +| --------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------- | +| **린트 검사** (`npm run lint`) | 코드 문법 오류, 미사용 변수 등 검출 | 빨간 ❌, 코드 고쳐서 다시 push | +| **단위 테스트** (`npm run test`) | 핵심 로직과 API 주변 회귀 테스트 실행 | 빨간 ❌, 실패 테스트 수정 후 다시 push | +| **빌드 테스트** (`npm run build`) | 프로젝트가 에러 없이 빌드되는지 확인 | 빨간 ❌, 빌드 에러 수정 후 다시 push | +| **E2E 테스트** (`npm run test:e2e -- --project=chromium`) | 핵심 사용자 플로우를 실제 브라우저로 검증 | 빨간 ❌, 사용자 흐름 수정 후 다시 push | +| **템플릿 검사** | 체크리스트 완료 여부, 본문 길이 검증 | 빨간 ❌, PR 설명을 보강해서 Edit | +| **팀원 승인 (Approve)** | 최소 1명 이상의 팀원이 코드를 읽고 ✅ Approve를 해야 함 | Merge 불가 상태 유지 | -네 가지 조건이 모두 달성되어야만 Merge 버튼이 활성화됩니다. (특히 팀원의 Approve는 필수 규칙입니다!) +모든 조건이 달성되어야만 Merge 버튼이 활성화됩니다. (특히 팀원의 Approve는 필수 규칙입니다!) --- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..09bfab8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +# MOIM 문서 인덱스 + +작성일: 2026-05-28 + +이 문서는 현재 repo에서 어떤 문서를 기준으로 읽어야 하는지 정리한 입구입니다. + +## 현재 기준 + +1. `docs/v2/user-flow.md` + - 최신 사용자 흐름과 수익화 넛지 위치를 설명합니다. +2. `docs/v2/prd.md` + - v2 제품 요구사항 초안입니다. 아직 `Draft` 상태라 구현 확정 문서로 보지는 않습니다. +3. `docs/v2/monetization-strategy.md` + - BM/요금제/확장 전략 정리본입니다. +4. `docs/v1/codex-work-context.md` + - 현재 개발 상태, 검증 명령, 다음 세션 시작 프롬프트를 담은 handoff 문서입니다. + +## 보존 문서 + +- `docs/v1/user-flow.md`: guest-first, invite-link-first 제품 원칙을 정리한 원본 기준 문서입니다. +- `docs/v1/provided-documents-summary.md`: 이전 제공 문서 요약입니다. + +## 읽는 순서 + +새 팀원이나 새 Codex 세션은 `README.md`를 본 뒤 이 파일을 읽고, 개발 작업 전에는 `docs/v1/codex-work-context.md`의 현재 repo 상태를 다시 확인합니다. diff --git a/docs/v1/codex-work-context.md b/docs/v1/codex-work-context.md index 6df82ee..2342895 100644 --- a/docs/v1/codex-work-context.md +++ b/docs/v1/codex-work-context.md @@ -6,13 +6,19 @@ ## 먼저 읽을 파일 -1. `docs/v1/user-flow.md` - - 실제 제품 흐름의 기준 문서다. 핵심 방향은 게스트 퍼스트, 초대 링크 문맥 우선이다. -2. `convention.md` +1. `README.md` + - 로컬 실행, 검증 명령, 현재 안정성 기준을 확인하는 첫 문서다. +2. `docs/README.md` + - `docs/v1`과 `docs/v2`의 역할을 구분하는 문서 인덱스다. +3. `docs/v2/user-flow.md` + - 최신 사용자 흐름과 수익화 연결안을 정리한 문서다. +4. `docs/v1/user-flow.md` + - guest-first, 초대 링크 문맥 우선이라는 제품 원칙의 원본 기준 문서다. +5. `convention.md` - 커밋, 브랜치, 이슈, PR 규칙의 기준이다. -3. `ARCHITECTURE.md` +6. `ARCHITECTURE.md` - 코드 구조와 테스트 전략을 볼 때의 기준 문서다. -4. `prototype/moim.html` +7. `prototype/moim.html` - 사용자 테스트용 단일 HTML 프로토타입의 중심 파일이다. ## 제품 방향 @@ -83,9 +89,9 @@ npm run test:e2e -- --project=chromium - `src/app/schedule/create/CreateScheduleClient.tsx`: 호스트가 모임을 만들고 참여자/호스트 링크를 받는다. - `src/app/schedule/[id]/ScheduleRoomClient.tsx`: 참여자가 이름과 가능 시간을 제출하고, host token이 있으면 결과를 본다. - `src/app/api/schedules/*`: 위 플로우를 위한 API route다. -- `src/lib/schedules/in-memory-store.ts`: 프로토타입/E2E용 in-memory schedule store다. +- `src/lib/schedules/store.ts`: Prisma/SQLite-backed schedule store다. host token 원문은 저장하지 않고 hash만 저장한다. -이 저장소는 운영 영속 저장소가 아니다. Supabase-backed schedule persistence를 붙일 때는 API contract를 유지하면서 저장소 구현을 교체한다. +이 저장소는 process memory가 아니라 로컬 SQLite DB에 남는다. Supabase-backed 운영 저장소로 확장할 때도 API contract를 유지하면서 저장소 구현과 배포 DB만 교체한다. ### 3. 캘린더/시간 계산 @@ -114,14 +120,14 @@ Google/iCloud/수동 입력/ICS는 provider raw data를 표준 시간 표현으 1. 이슈 #30의 레포 위생/CI 정리를 완료하고 PR을 만든다. 2. `dev` 기준 CI가 `lint`, `test`, `build`, Chromium e2e를 모두 실행하는지 확인한다. -3. Supabase-backed schedule persistence를 별도 이슈로 분리한다. +3. Supabase-backed 운영 저장소 전환을 별도 이슈로 분리한다. 4. 참여자 guest flow와 host auth flow가 섞이지 않도록 API와 UI 경계를 유지한다. ## 다음 세션 시작 프롬프트 ```text C:\Users\kksu1\Dev\MOIM에서 작업해줘. -먼저 docs/v1/codex-work-context.md와 convention.md를 읽고 현재 개발 현황과 협업 규칙을 파악해. +먼저 README.md, docs/README.md, docs/v1/codex-work-context.md, convention.md를 읽고 현재 개발 현황과 협업 규칙을 파악해. 기존 작업트리에 수정/미추적 파일이 있으면 함부로 되돌리지 말고, git status와 GitHub PR 상태를 다시 확인해. 변경 후에는 npm run lint, npm run test, npm run build, 가능하면 npm run test:e2e -- --project=chromium으로 검증해. ``` diff --git a/docs/v1/provided-documents-summary.md b/docs/v1/provided-documents-summary.md index 807bac1..03b45e4 100644 --- a/docs/v1/provided-documents-summary.md +++ b/docs/v1/provided-documents-summary.md @@ -20,17 +20,17 @@ ## 문서 목록 -| 문서 | 현재 위치 | 성격 | 상태 | -|---|---|---|---| -| 학생창업유망팀 300+ 사업계획서 | `MOIM_2026_학생창업유망팀300_사업계획서.docx` | 제출용 사업계획서 | 원본 DOCX 존재 | -| 프로젝트 아키텍처 가이드 | `ARCHITECTURE.md` | 개발 구조 설명 | 원본 MD 존재 | -| 협업 컨벤션 가이드 | `convention.md` | 커밋, 브랜치, 이슈, PR 규칙 | 원본 MD 존재 | -| 도메인 컨텍스트 | `.agents/docs/1_DOMAIN_CONTEXT.md` | 에이전트용 프로젝트 맥락 | 수정 중 | -| 실행 계획 | `.agents/docs/2_EXECUTION_PLAN.md` | Phase별 구현 로드맵 | 수정 중 | -| 파일 인덱스 | `.agents/docs/3_FILE_INDEX.md` | 파일 역할 지도 | 원본 MD 존재 | -| 제품 사용자 흐름 문서 | `docs/v1/user-flow.md` | 게스트 퍼스트 실제 제품 흐름 | 원본 MD 존재 | -| 개발 현황 인수인계 | `docs/v1/codex-work-context.md` | 현재 개발 상태와 검증 기준 | 원본 MD 존재 | -| AI calendar assistant PRD 리뷰 맥락 | 현재 원본 파일 미확인 | 제품/수익화/범위 리뷰 | 메모리 기반 맥락만 확인 | +| 문서 | 현재 위치 | 성격 | 상태 | +| ----------------------------------- | --------------------------------------------- | ---------------------------- | ----------------------- | +| 학생창업유망팀 300+ 사업계획서 | `MOIM_2026_학생창업유망팀300_사업계획서.docx` | 제출용 사업계획서 | 원본 DOCX 존재 | +| 프로젝트 아키텍처 가이드 | `ARCHITECTURE.md` | 개발 구조 설명 | 원본 MD 존재 | +| 협업 컨벤션 가이드 | `convention.md` | 커밋, 브랜치, 이슈, PR 규칙 | 원본 MD 존재 | +| 도메인 컨텍스트 | `.agents/docs/1_DOMAIN_CONTEXT.md` | 에이전트용 프로젝트 맥락 | 수정 중 | +| 실행 계획 | `.agents/docs/2_EXECUTION_PLAN.md` | Phase별 구현 로드맵 | 수정 중 | +| 파일 인덱스 | `.agents/docs/3_FILE_INDEX.md` | 파일 역할 지도 | 원본 MD 존재 | +| 제품 사용자 흐름 문서 | `docs/v1/user-flow.md` | 게스트 퍼스트 실제 제품 흐름 | 원본 MD 존재 | +| 개발 현황 인수인계 | `docs/v1/codex-work-context.md` | 현재 개발 상태와 검증 기준 | 원본 MD 존재 | +| AI calendar assistant PRD 리뷰 맥락 | 현재 원본 파일 미확인 | 제품/수익화/범위 리뷰 | 메모리 기반 맥락만 확인 | ## 사업계획서 핵심 내용 @@ -155,7 +155,7 @@ MOIM은 대학생 팀 활동에서 반복되는 일정 조율 문제를 줄이 구조 원칙: - `src/app`은 사용자 플로우 기반 라우팅을 담당한다. -- `src/components/ui`는 범용 UI, `src/components/schedule`은 MOIM 도메인 컴포넌트로 분리한다. +- `src/components/ui`는 범용 UI를 담당하고, 현재 스케줄 화면 UI는 `src/app/schedule/*` 라우트 아래에 둔다. - `src/lib/scheduling`은 React와 분리된 순수 함수 중심의 핵심 로직 영역이다. - 테스트는 구현 파일 가까이에 두는 코로케이션 방식을 쓴다. - `Unit`은 핵심 로직, `Integration`은 컴포넌트 상호작용, `E2E`는 사용자 전체 플로우를 맡는다. diff --git a/package-lock.json b/package-lock.json index 4d49280..75a0778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@base-ui/react": "^1.4.0", - "@prisma/client": "^5.22.0", + "@prisma/client": "6.19.3", "@supabase/ssr": "^0.10.3", "@supabase/supabase-js": "^2.106.1", "bcryptjs": "^3.0.3", @@ -19,8 +19,8 @@ "fast-xml-parser": "^5.5.12", "jose": "^6.2.3", "lucide-react": "^1.8.0", - "next": "^14.2.35", - "prisma": "^5.22.0", + "next": "15.5.18", + "prisma": "6.19.3", "react": "^18", "react-dom": "^18", "shadcn": "^4.2.0", @@ -40,17 +40,18 @@ "@types/react": "^18", "@types/react-dom": "^18", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.7", "dotenv": "^17.4.2", "eslint": "^8", - "eslint-config-next": "14.2.35", + "eslint-config-next": "15.5.18", "husky": "^9.1.7", "jsdom": "^29.0.2", "lint-staged": "^16.4.0", - "postcss": "^8", + "postcss": "8.5.15", "prettier": "^3.8.2", "tailwindcss": "^3.4.1", "typescript": "^5", - "vitest": "^4.1.4" + "vitest": "4.1.7" } }, "node_modules/@adobe/css-tools": { @@ -140,6 +141,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -370,18 +372,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -410,12 +412,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -550,13 +552,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -616,6 +618,16 @@ } } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1000,6 +1012,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1048,6 +1061,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1244,7 +1258,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1455,6 +1468,472 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -1581,53 +2060,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1744,25 +2176,55 @@ } }, "node_modules/@next/env": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", - "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.18.tgz", + "integrity": "sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz", - "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.18.tgz", + "integrity": "sha512-w4MYq8M26a8PNrfto0JosLf5/3ssln1rsyP96g2DkC8uFVymStM5DLSz5ElxxrPRg2XnTMnFo3kREFlhYvxhWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "license": "MIT", "dependencies": { - "glob": "10.3.10" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", - "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.18.tgz", + "integrity": "sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==", "cpu": [ "arm64" ], @@ -1776,9 +2238,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", - "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.18.tgz", + "integrity": "sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==", "cpu": [ "x64" ], @@ -1792,9 +2254,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", - "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.18.tgz", + "integrity": "sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==", "cpu": [ "arm64" ], @@ -1808,9 +2270,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", - "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.18.tgz", + "integrity": "sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==", "cpu": [ "arm64" ], @@ -1824,9 +2286,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", - "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.18.tgz", + "integrity": "sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==", "cpu": [ "x64" ], @@ -1840,9 +2302,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", - "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.18.tgz", + "integrity": "sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==", "cpu": [ "x64" ], @@ -1856,9 +2318,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", - "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.18.tgz", + "integrity": "sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==", "cpu": [ "arm64" ], @@ -1871,26 +2333,10 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", - "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", - "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.18.tgz", + "integrity": "sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==", "cpu": [ "x64" ], @@ -1908,6 +2354,7 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2031,23 +2478,13 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@playwright/test": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.59.1" }, @@ -2059,66 +2496,150 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { - "prisma": "*" + "prisma": "*", + "typescript": ">=5.1.0" }, "peerDependenciesMeta": { "prisma": { "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/config/node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true } } }, + "node_modules/@prisma/config/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@prisma/config/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@prisma/config/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" } }, "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" } }, "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.19.3" } }, "node_modules/@rolldown/binding-android-arm64": { @@ -2344,29 +2865,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", @@ -2492,7 +2990,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, "license": "MIT" }, "node_modules/@supabase/auth-js": { @@ -2593,6 +3090,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.106.1", "@supabase/functions-js": "2.106.1", @@ -2604,20 +3102,13 @@ "node": ">=20.0.0" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@testing-library/dom": { @@ -2773,8 +3264,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/bcryptjs": { "version": "2.4.6", @@ -2802,9 +3292,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2821,6 +3311,7 @@ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2838,6 +3329,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2849,6 +3341,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2910,6 +3403,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -3449,17 +3943,49 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz", + "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.7", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.7", + "vitest": "4.1.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3468,13 +3994,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3495,9 +4021,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { @@ -3508,13 +4034,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.7", "pathe": "^2.0.3" }, "funding": { @@ -3522,14 +4048,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3538,9 +4064,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", "funding": { @@ -3548,13 +4074,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3581,6 +4107,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3933,6 +4460,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", + "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4096,6 +4642,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4125,17 +4672,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4297,6 +4833,15 @@ "node": ">= 6" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4541,6 +5086,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -4652,6 +5212,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4894,6 +5455,15 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -4970,6 +5540,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4989,11 +5565,17 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5040,8 +5622,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dot-prop": { "version": "5.3.0", @@ -5082,13 +5663,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/eciesjs": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", @@ -5112,6 +5686,16 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.336", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", @@ -5125,6 +5709,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5394,6 +5987,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5445,25 +6039,25 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz", - "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.18.tgz", + "integrity": "sha512-HuoJU6uUPD00eyiud78IBnT4HLhztFj2V+ild2Uon5ZUrYZKe0Olu2QRD99e9IgL4/H1eg5Onka3BsfRW2U0Xw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.35", - "@rushstack/eslint-patch": "^1.3.3", + "@next/eslint-plugin-next": "15.5.18", + "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -5563,6 +6157,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5698,16 +6293,16 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0-canary-7118f5dd7-20230705", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", - "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -5952,6 +6547,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6008,6 +6604,34 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6275,23 +6899,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -6540,6 +7147,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/git-raw-commits": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", @@ -6557,30 +7181,6 @@ "node": ">=18" } }, - "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6594,32 +7194,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -6806,6 +7380,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6823,6 +7398,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7638,6 +8220,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7656,30 +8277,10 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -7718,6 +8319,7 @@ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", @@ -8561,7 +9163,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8576,6 +9177,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8744,16 +9373,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8904,41 +9523,40 @@ } }, "node_modules/next": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", - "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.18.tgz", + "integrity": "sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==", "license": "MIT", "dependencies": { - "@next/env": "14.2.35", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", + "@next/env": "15.5.18", + "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1" + "styled-jsx": "5.1.6" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=18.17.0" + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.33", - "@next/swc-darwin-x64": "14.2.33", - "@next/swc-linux-arm64-gnu": "14.2.33", - "@next/swc-linux-arm64-musl": "14.2.33", - "@next/swc-linux-x64-gnu": "14.2.33", - "@next/swc-linux-x64-musl": "14.2.33", - "@next/swc-win32-arm64-msvc": "14.2.33", - "@next/swc-win32-ia32-msvc": "14.2.33", - "@next/swc-win32-x64-msvc": "14.2.33" + "@next/swc-darwin-arm64": "15.5.18", + "@next/swc-darwin-x64": "15.5.18", + "@next/swc-linux-arm64-gnu": "15.5.18", + "@next/swc-linux-arm64-musl": "15.5.18", + "@next/swc-linux-x64-gnu": "15.5.18", + "@next/swc-linux-x64-musl": "15.5.18", + "@next/swc-win32-arm64-msvc": "15.5.18", + "@next/swc-win32-x64-msvc": "15.5.18", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -8948,37 +9566,12 @@ "@playwright/test": { "optional": true }, - "sass": { + "babel-plugin-react-compiler": { "optional": true - } - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "sass": { + "optional": true } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" } }, "node_modules/node-domexception": { @@ -9048,6 +9641,12 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -9092,6 +9691,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9243,6 +9865,12 @@ ], "license": "MIT" }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9579,30 +10207,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -9613,7 +10217,12 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, "node_modules/picocolors": { @@ -9663,6 +10272,17 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/playwright": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", @@ -9724,6 +10344,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -9946,7 +10567,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9962,7 +10582,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -9986,36 +10605,29 @@ } }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, - "optionalDependencies": { - "fsevents": "2.3.3" - } - }, - "node_modules/prisma/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/prompts": { @@ -10082,6 +10694,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -10141,11 +10769,22 @@ "node": ">= 0.10" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10158,6 +10797,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10171,8 +10811,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -10619,7 +11258,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10796,6 +11435,51 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11018,14 +11702,6 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -11042,86 +11718,6 @@ "node": ">=0.6.19" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -11276,20 +11872,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -11350,9 +11932,9 @@ "license": "MIT" }, "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { "client-only": "0.0.1" @@ -11361,7 +11943,7 @@ "node": ">= 12.0.0" }, "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "peerDependenciesMeta": { "@babel/core": { @@ -11591,7 +12173,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -11883,6 +12464,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12080,6 +12662,7 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -12168,19 +12751,20 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -12208,12 +12792,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -12445,117 +13029,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -12754,6 +13227,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 23fedca..6ce2c70 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,25 @@ "version": "0.1.0", "private": true, "scripts": { + "predev": "npm run db:migrate", "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test", + "db:migrate": "node scripts/ensure-sqlite-schema.mjs", + "lint": "eslint src e2e --ext .ts,.tsx", + "pretest": "npm run db:migrate", + "test": "node scripts/with-database-url.mjs vitest run", + "test:watch": "node scripts/with-database-url.mjs vitest", + "pretest:coverage": "npm run db:migrate", + "test:coverage": "node scripts/with-database-url.mjs vitest run --coverage", + "pretest:e2e": "npm run db:migrate", + "test:e2e": "node scripts/with-database-url.mjs playwright test", "prepare": "husky", "postinstall": "prisma generate" }, "dependencies": { "@base-ui/react": "^1.4.0", - "@prisma/client": "^5.22.0", + "@prisma/client": "6.19.3", "@supabase/ssr": "^0.10.3", "@supabase/supabase-js": "^2.106.1", "bcryptjs": "^3.0.3", @@ -25,8 +30,8 @@ "fast-xml-parser": "^5.5.12", "jose": "^6.2.3", "lucide-react": "^1.8.0", - "next": "^14.2.35", - "prisma": "^5.22.0", + "next": "15.5.18", + "prisma": "6.19.3", "react": "^18", "react-dom": "^18", "shadcn": "^4.2.0", @@ -46,17 +51,21 @@ "@types/react": "^18", "@types/react-dom": "^18", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.7", "dotenv": "^17.4.2", "eslint": "^8", - "eslint-config-next": "14.2.35", + "eslint-config-next": "15.5.18", "husky": "^9.1.7", "jsdom": "^29.0.2", "lint-staged": "^16.4.0", - "postcss": "^8", + "postcss": "8.5.15", "prettier": "^3.8.2", "tailwindcss": "^3.4.1", "typescript": "^5", - "vitest": "^4.1.4" + "vitest": "4.1.7" + }, + "overrides": { + "postcss": "8.5.15" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ diff --git a/playwright.config.ts b/playwright.config.ts index 24135fa..5cff178 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,8 @@ import { defineConfig, devices } from "@playwright/test"; +const e2ePort = process.env.PLAYWRIGHT_PORT ?? "3100"; +const baseURL = `http://localhost:${e2ePort}`; + /** * Playwright E2E 설정 * @@ -18,7 +21,7 @@ export default defineConfig({ reporter: "html", use: { - baseURL: "http://localhost:3000", + baseURL, trace: "on-first-retry", // 실패 시 트레이스 기록 (디버깅용) }, @@ -39,8 +42,8 @@ export default defineConfig({ // Next.js 개발 서버를 자동으로 띄워서 테스트 webServer: { - command: "npm run dev", - url: "http://localhost:3000", + command: `npm run dev -- --port ${e2ePort}`, + url: baseURL, reuseExistingServer: !process.env.CI, }, }); diff --git a/prisma/migrations/20260528001000_add_schedule_persistence/migration.sql b/prisma/migrations/20260528001000_add_schedule_persistence/migration.sql new file mode 100644 index 0000000..37da859 --- /dev/null +++ b/prisma/migrations/20260528001000_add_schedule_persistence/migration.sql @@ -0,0 +1,25 @@ +-- Persist invite-based schedule sessions outside the Next.js process. +CREATE TABLE "Schedule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "hostTokenHash" TEXT NOT NULL, + "title" TEXT NOT NULL, + "durationMinutes" INTEGER NOT NULL, + "candidateDays" TEXT NOT NULL, + "candidateStartHour" INTEGER NOT NULL, + "candidateEndHour" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "ScheduleParticipant" ( + "id" TEXT NOT NULL PRIMARY KEY, + "scheduleId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "available" TEXT NOT NULL, + "submittedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ScheduleParticipant_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "Schedule" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "Schedule_hostTokenHash_key" ON "Schedule"("hostTokenHash"); + +CREATE INDEX "ScheduleParticipant_scheduleId_idx" ON "ScheduleParticipant"("scheduleId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b623f77..e2dc86d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,3 +35,27 @@ model SocialAccount { @@unique([provider, providerUserId]) } + +model Schedule { + id String @id + hostTokenHash String @unique + title String + durationMinutes Int + candidateDays String + candidateStartHour Int + candidateEndHour Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + participants ScheduleParticipant[] +} + +model ScheduleParticipant { + id String @id + scheduleId String + schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade) + name String + available String + submittedAt DateTime @default(now()) + + @@index([scheduleId]) +} diff --git a/scripts/ensure-sqlite-schema.mjs b/scripts/ensure-sqlite-schema.mjs new file mode 100644 index 0000000..c74b1d1 --- /dev/null +++ b/scripts/ensure-sqlite-schema.mjs @@ -0,0 +1,83 @@ +import { PrismaClient } from "@prisma/client"; + +if (process.env.NODE_ENV !== "production") { + process.env.DATABASE_URL ||= "file:./dev.db"; +} + +const prisma = new PrismaClient(); + +await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT, + "phoneNumber" TEXT, + "nickname" TEXT NOT NULL, + "passwordHash" TEXT, + "isAgeOver14" BOOLEAN, + "termsAgreedAt" DATETIME, + "privacyAgreedAt" DATETIME, + "marketingAgreed" BOOLEAN NOT NULL DEFAULT false, + "eventSmsAgreed" BOOLEAN NOT NULL DEFAULT false, + "profileCompleted" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL + ) +`); + +await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "SocialAccount" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerUserId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "SocialAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE + ) +`); + +await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "Schedule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "hostTokenHash" TEXT NOT NULL, + "title" TEXT NOT NULL, + "durationMinutes" INTEGER NOT NULL, + "candidateDays" TEXT NOT NULL, + "candidateStartHour" INTEGER NOT NULL, + "candidateEndHour" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL + ) +`); + +await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "ScheduleParticipant" ( + "id" TEXT NOT NULL PRIMARY KEY, + "scheduleId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "available" TEXT NOT NULL, + "submittedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ScheduleParticipant_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "Schedule" ("id") ON DELETE CASCADE ON UPDATE CASCADE + ) +`); + +await prisma.$executeRawUnsafe( + `CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User"("email")`, +); +await prisma.$executeRawUnsafe( + `CREATE UNIQUE INDEX IF NOT EXISTS "User_phoneNumber_key" ON "User"("phoneNumber")`, +); +await prisma.$executeRawUnsafe( + `CREATE UNIQUE INDEX IF NOT EXISTS "User_nickname_key" ON "User"("nickname")`, +); +await prisma.$executeRawUnsafe( + `CREATE UNIQUE INDEX IF NOT EXISTS "SocialAccount_provider_providerUserId_key" ON "SocialAccount"("provider", "providerUserId")`, +); +await prisma.$executeRawUnsafe( + `CREATE UNIQUE INDEX IF NOT EXISTS "Schedule_hostTokenHash_key" ON "Schedule"("hostTokenHash")`, +); +await prisma.$executeRawUnsafe( + `CREATE INDEX IF NOT EXISTS "ScheduleParticipant_scheduleId_idx" ON "ScheduleParticipant"("scheduleId")`, +); + +await prisma.$disconnect(); diff --git a/scripts/with-database-url.mjs b/scripts/with-database-url.mjs new file mode 100644 index 0000000..3475f64 --- /dev/null +++ b/scripts/with-database-url.mjs @@ -0,0 +1,46 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +process.env.DATABASE_URL ||= "file:./dev.db"; + +const [command, ...args] = process.argv.slice(2); + +if (!command) { + console.error( + "Usage: node scripts/with-database-url.mjs [...args]", + ); + process.exit(1); +} + +const localBin = resolve( + "node_modules", + ".bin", + process.platform === "win32" ? `${command}.cmd` : command, +); +const executable = existsSync(localBin) ? localBin : command; + +const child = + process.platform === "win32" + ? spawn([executable, ...args].map(quoteForShell).join(" "), { + env: process.env, + shell: true, + stdio: "inherit", + }) + : spawn(executable, args, { + env: process.env, + stdio: "inherit", + }); + +child.on("exit", (code, signal) => { + if (signal) { + console.error(`Command terminated by ${signal}`); + process.exit(1); + } + process.exit(code ?? 1); +}); + +function quoteForShell(value) { + if (/^[A-Za-z0-9_./:=\\-]+$/.test(value)) return value; + return `"${value.replace(/"/g, '\\"')}"`; +} diff --git a/src/app/api/schedules/[id]/availability/route.ts b/src/app/api/schedules/[id]/availability/route.ts index 00da8e3..3e724de 100644 --- a/src/app/api/schedules/[id]/availability/route.ts +++ b/src/app/api/schedules/[id]/availability/route.ts @@ -2,24 +2,25 @@ import { NextRequest, NextResponse } from "next/server"; import { addParticipantAvailability, getSchedulePublic, -} from "@/lib/schedules/in-memory-store"; +} from "@/lib/schedules/store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function POST( request: NextRequest, - { params }: { params: { id: string } }, + { params }: { params: Promise<{ id: string }> }, ) { try { - const participant = addParticipantAvailability( - params.id, + const { id } = await params; + const participant = await addParticipantAvailability( + id, await request.json(), ); return NextResponse.json( { participant, - schedule: getSchedulePublic(params.id), + schedule: await getSchedulePublic(id), }, { status: 201 }, ); diff --git a/src/app/api/schedules/[id]/route.ts b/src/app/api/schedules/[id]/route.ts index 1ded162..8295d39 100644 --- a/src/app/api/schedules/[id]/route.ts +++ b/src/app/api/schedules/[id]/route.ts @@ -1,20 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; -import { - getScheduleForHost, - getSchedulePublic, -} from "@/lib/schedules/in-memory-store"; +import { getScheduleForHost, getSchedulePublic } from "@/lib/schedules/store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET( request: NextRequest, - { params }: { params: { id: string } }, + { params }: { params: Promise<{ id: string }> }, ) { + const { id } = await params; const hostToken = request.nextUrl.searchParams.get("hostToken"); if (hostToken) { - const hostSchedule = getScheduleForHost(params.id, hostToken); + const hostSchedule = await getScheduleForHost(id, hostToken); if (!hostSchedule) { return NextResponse.json( { error: "invalid host token" }, @@ -24,7 +22,7 @@ export async function GET( return NextResponse.json({ schedule: hostSchedule }); } - const schedule = getSchedulePublic(params.id); + const schedule = await getSchedulePublic(id); if (!schedule) { return NextResponse.json({ error: "schedule not found" }, { status: 404 }); } diff --git a/src/app/api/schedules/route.ts b/src/app/api/schedules/route.ts index 5984156..b5b4b84 100644 --- a/src/app/api/schedules/route.ts +++ b/src/app/api/schedules/route.ts @@ -1,16 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; -import { - createSchedule, - getSchedulePublic, -} from "@/lib/schedules/in-memory-store"; +import { createSchedule, getSchedulePublic } from "@/lib/schedules/store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function POST(request: NextRequest) { try { - const created = createSchedule(await request.json()); - const schedule = getSchedulePublic(created.id); + const created = await createSchedule(await request.json()); + const schedule = await getSchedulePublic(created.id); return NextResponse.json( { diff --git a/src/app/schedule/[id]/page.tsx b/src/app/schedule/[id]/page.tsx index 23db9f0..38d0d23 100644 --- a/src/app/schedule/[id]/page.tsx +++ b/src/app/schedule/[id]/page.tsx @@ -1,16 +1,14 @@ import { ScheduleRoomClient } from "./ScheduleRoomClient"; -export default function ScheduleParticipantPage({ +export default async function ScheduleParticipantPage({ params, searchParams, }: { - params: { id: string }; - searchParams: { hostToken?: string }; + params: Promise<{ id: string }>; + searchParams: Promise<{ hostToken?: string }>; }) { - return ( - - ); + const { id } = await params; + const { hostToken } = await searchParams; + + return ; } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index a309320..d7fd4bb 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,5 +1,9 @@ import { PrismaClient } from "@prisma/client"; +if (process.env.NODE_ENV !== "production") { + process.env.DATABASE_URL ||= "file:./dev.db"; +} + const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); diff --git a/src/lib/schedules/__tests__/in-memory-store.test.ts b/src/lib/schedules/__tests__/store.test.ts similarity index 58% rename from src/lib/schedules/__tests__/in-memory-store.test.ts rename to src/lib/schedules/__tests__/store.test.ts index 3b31aaf..00abba5 100644 --- a/src/lib/schedules/__tests__/in-memory-store.test.ts +++ b/src/lib/schedules/__tests__/store.test.ts @@ -6,15 +6,15 @@ import { createSchedule, getScheduleForHost, getSchedulePublic, -} from "../in-memory-store"; +} from "../store"; -describe("in-memory schedule store", () => { - afterEach(() => { - clearSchedules(); +describe("schedule store", () => { + afterEach(async () => { + await clearSchedules(); }); - test("creates an unguessable schedule id and keeps the host token out of public data", () => { - const created = createSchedule({ + test("creates an unguessable schedule id and keeps the host token out of public data", async () => { + const created = await createSchedule({ title: "제품 회의", durationMinutes: 60, candidateDays: ["MON", "TUE"], @@ -25,7 +25,7 @@ describe("in-memory schedule store", () => { expect(created.id).toMatch(/^[A-Za-z0-9_-]{20,}$/); expect(created.hostToken).toMatch(/^[A-Za-z0-9_-]{32,}$/); - const publicSchedule = getSchedulePublic(created.id); + const publicSchedule = await getSchedulePublic(created.id); expect(publicSchedule).toMatchObject({ id: created.id, @@ -36,8 +36,24 @@ describe("in-memory schedule store", () => { expect(publicSchedule).not.toHaveProperty("participants"); }); - test("requires the host token before returning participant details and common slots", () => { - const created = createSchedule({ + test("persists schedules across fresh reads", async () => { + const created = await createSchedule({ + title: "저장 확인", + durationMinutes: 45, + candidateDays: ["WED"], + candidateStartHour: 13, + candidateEndHour: 17, + }); + + const firstRead = await getSchedulePublic(created.id); + const secondRead = await getSchedulePublic(created.id); + + expect(firstRead?.title).toBe("저장 확인"); + expect(secondRead).toEqual(firstRead); + }); + + test("requires the host token before returning participant details and common slots", async () => { + const created = await createSchedule({ title: "인터뷰 일정", durationMinutes: 60, candidateDays: ["MON"], @@ -45,18 +61,21 @@ describe("in-memory schedule store", () => { candidateEndHour: 18, }); - addParticipantAvailability(created.id, { + await addParticipantAvailability(created.id, { name: "민지", available: [{ day: "MON", startHour: 10, endHour: 14 }], }); - addParticipantAvailability(created.id, { + await addParticipantAvailability(created.id, { name: "준호", available: [{ day: "MON", startHour: 12, endHour: 16 }], }); - expect(getScheduleForHost(created.id, "wrong-token")).toBeNull(); + expect(await getScheduleForHost(created.id, "wrong-token")).toBeNull(); - const hostSchedule = getScheduleForHost(created.id, created.hostToken); + const hostSchedule = await getScheduleForHost( + created.id, + created.hostToken, + ); expect(hostSchedule?.participants.map((p) => p.name)).toEqual([ "민지", @@ -68,8 +87,8 @@ describe("in-memory schedule store", () => { expect(hostSchedule).not.toHaveProperty("hostToken"); }); - test("rejects participant slots outside the schedule candidate window", () => { - const created = createSchedule({ + test("rejects participant slots outside the schedule candidate window", async () => { + const created = await createSchedule({ title: "저녁 약속", durationMinutes: 60, candidateDays: ["FRI"], @@ -82,16 +101,16 @@ describe("in-memory schedule store", () => { endHour: 20, }; - expect(() => + await expect( addParticipantAvailability(created.id, { name: "수아", available: [invalidSlot], }), - ).toThrow("candidate window"); + ).rejects.toThrow("candidate window"); }); - test("rejects empty participant names and empty availability", () => { - const created = createSchedule({ + test("rejects empty participant names", async () => { + const created = await createSchedule({ title: "스터디", durationMinutes: 60, candidateDays: ["SAT"], @@ -99,18 +118,28 @@ describe("in-memory schedule store", () => { candidateEndHour: 14, }); - expect(() => + await expect( addParticipantAvailability(created.id, { name: "", available: [{ day: "SAT", startHour: 10, endHour: 11 }], }), - ).toThrow("name"); + ).rejects.toThrow("name"); + }); + + test("rejects empty availability", async () => { + const created = await createSchedule({ + title: "스터디", + durationMinutes: 60, + candidateDays: ["SAT"], + candidateStartHour: 10, + candidateEndHour: 14, + }); - expect(() => + await expect( addParticipantAvailability(created.id, { name: "하린", available: [], }), - ).toThrow("availability"); + ).rejects.toThrow("availability"); }); }); diff --git a/src/lib/schedules/in-memory-store.ts b/src/lib/schedules/store.ts similarity index 51% rename from src/lib/schedules/in-memory-store.ts rename to src/lib/schedules/store.ts index 4277dc9..c4010e3 100644 --- a/src/lib/schedules/in-memory-store.ts +++ b/src/lib/schedules/store.ts @@ -1,13 +1,15 @@ -import { randomBytes, timingSafeEqual } from "crypto"; +import { createHash, randomBytes, timingSafeEqual } from "crypto"; +import type { Prisma } from "@prisma/client"; import type { DayCode, ParticipantAvailability, TimeSlot, } from "@/types/schedule"; +import { prisma } from "@/lib/prisma"; import { findCommonSlots } from "@/lib/scheduling/availability"; import { mergeOverlapping, sortSlots } from "@/lib/scheduling/time-slot"; -export interface CreateInMemoryScheduleInput { +export interface CreateScheduleInput { title: string; durationMinutes: number; candidateDays: DayCode[]; @@ -20,7 +22,7 @@ export interface AddParticipantAvailabilityInput { available: TimeSlot[]; } -export interface CreatedInMemorySchedule { +export interface CreatedSchedule { id: string; hostToken: string; } @@ -48,106 +50,104 @@ export interface HostSchedule extends PublicSchedule { commonSlots: TimeSlot[]; } -interface ScheduleRecord { - id: string; - hostToken: string; - title: string; - durationMinutes: number; - candidateDays: DayCode[]; - candidateStartHour: number; - candidateEndHour: number; - participants: ScheduleParticipant[]; - createdAt: string; -} +type ScheduleWithParticipants = Prisma.ScheduleGetPayload<{ + include: { participants: true }; +}>; -interface ScheduleStore { - schedules: Map; -} +type NormalizedScheduleInput = Omit & { + candidateDays: DayCode[]; +}; -const STORE_KEY = "__moimInMemoryScheduleStore"; const VALID_DAYS: DayCode[] = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]; -export function createSchedule( - input: CreateInMemoryScheduleInput, -): CreatedInMemorySchedule { +export async function createSchedule( + input: CreateScheduleInput, +): Promise { const schedule = normalizeScheduleInput(input); const id = createToken(18); const hostToken = createToken(32); - getStore().schedules.set(id, { - ...schedule, - id, - hostToken, - participants: [], - createdAt: new Date().toISOString(), + await prisma.schedule.create({ + data: { + ...schedule, + id, + hostTokenHash: hashToken(hostToken), + candidateDays: JSON.stringify(schedule.candidateDays), + }, }); return { id, hostToken }; } -export function getSchedulePublic(id: string): PublicSchedule | null { - const schedule = getStore().schedules.get(id); +export async function getSchedulePublic( + id: string, +): Promise { + const schedule = await prisma.schedule.findUnique({ + where: { id }, + include: { participants: true }, + }); if (!schedule) return null; return toPublicSchedule(schedule); } -export function getScheduleForHost( +export async function getScheduleForHost( id: string, hostToken: string, -): HostSchedule | null { - const schedule = getStore().schedules.get(id); - if (!schedule || !tokenMatches(schedule.hostToken, hostToken)) return null; +): Promise { + const schedule = await prisma.schedule.findUnique({ + where: { id }, + include: { participants: true }, + }); + if (!schedule || !tokenMatches(schedule.hostTokenHash, hostToken)) { + return null; + } - const participantAvailability: ParticipantAvailability[] = - schedule.participants.map((participant) => ({ + const participants = schedule.participants.map(toScheduleParticipant); + const participantAvailability: ParticipantAvailability[] = participants.map( + (participant) => ({ userId: participant.id, available: participant.available, - })); + }), + ); return { ...toPublicSchedule(schedule), - participants: schedule.participants.map(copyParticipant), + participants, commonSlots: findCommonSlots(participantAvailability), }; } -export function addParticipantAvailability( +export async function addParticipantAvailability( scheduleId: string, input: AddParticipantAvailabilityInput, -): ScheduleParticipant { - const schedule = getStore().schedules.get(scheduleId); +): Promise { + const schedule = await prisma.schedule.findUnique({ + where: { id: scheduleId }, + }); if (!schedule) throw new Error("schedule not found"); - const participant: ScheduleParticipant = { - id: createToken(12), - name: normalizeParticipantName(input.name), - available: normalizeAvailability(schedule, input.available), - submittedAt: new Date().toISOString(), - }; + const participant = await prisma.scheduleParticipant.create({ + data: { + id: createToken(12), + scheduleId, + name: normalizeParticipantName(input.name), + available: JSON.stringify( + normalizeAvailability(schedule, input.available), + ), + }, + }); - schedule.participants.push(participant); - return copyParticipant(participant); + return toScheduleParticipant(participant); } -export function clearSchedules() { - getStore().schedules.clear(); -} - -function getStore(): ScheduleStore { - const globalStore = globalThis as typeof globalThis & { - [STORE_KEY]?: ScheduleStore; - }; - - if (!globalStore[STORE_KEY]) { - globalStore[STORE_KEY] = { schedules: new Map() }; - } - - return globalStore[STORE_KEY]; +export async function clearSchedules() { + await prisma.scheduleParticipant.deleteMany(); + await prisma.schedule.deleteMany(); } function normalizeScheduleInput( - input: CreateInMemoryScheduleInput, -): Omit { + input: CreateScheduleInput, +): NormalizedScheduleInput { const title = input.title.trim(); if (title.length < 2 || title.length > 80) { throw new Error("title must be between 2 and 80 characters"); @@ -193,17 +193,22 @@ function normalizeParticipantName(name: string): string { } function normalizeAvailability( - schedule: ScheduleRecord, + schedule: { + candidateDays: string; + candidateStartHour: number; + candidateEndHour: number; + }, available: TimeSlot[], ): TimeSlot[] { if (available.length === 0) { throw new Error("availability must not be empty"); } + const candidateDays = parseDayCodes(schedule.candidateDays); for (const slot of available) { validateHourRange(slot.startHour, slot.endHour); if ( - !schedule.candidateDays.includes(slot.day) || + !candidateDays.includes(slot.day) || slot.startHour < schedule.candidateStartHour || slot.endHour > schedule.candidateEndHour ) { @@ -226,35 +231,81 @@ function validateHourRange(startHour: number, endHour: number) { } } -function toPublicSchedule(schedule: ScheduleRecord): PublicSchedule { +function toPublicSchedule(schedule: ScheduleWithParticipants): PublicSchedule { return { id: schedule.id, title: schedule.title, durationMinutes: schedule.durationMinutes, - candidateDays: [...schedule.candidateDays], + candidateDays: parseDayCodes(schedule.candidateDays), candidateStartHour: schedule.candidateStartHour, candidateEndHour: schedule.candidateEndHour, participantCount: schedule.participants.length, - createdAt: schedule.createdAt, + createdAt: schedule.createdAt.toISOString(), }; } -function copyParticipant( - participant: ScheduleParticipant, -): ScheduleParticipant { +function toScheduleParticipant(participant: { + id: string; + name: string; + available: string; + submittedAt: Date; +}): ScheduleParticipant { return { - ...participant, - available: participant.available.map((slot) => ({ ...slot })), + id: participant.id, + name: participant.name, + available: parseTimeSlots(participant.available), + submittedAt: participant.submittedAt.toISOString(), }; } +function parseDayCodes(value: string): DayCode[] { + const parsed = parseJson(value); + if (!Array.isArray(parsed)) return []; + return parsed.filter((day): day is DayCode => + VALID_DAYS.includes(day as DayCode), + ); +} + +function parseTimeSlots(value: string): TimeSlot[] { + const parsed = parseJson(value); + if (!Array.isArray(parsed)) return []; + return parsed + .filter((slot): slot is TimeSlot => isTimeSlot(slot)) + .map((slot) => ({ ...slot })); +} + +function parseJson(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return undefined; + } +} + +function isTimeSlot(value: unknown): value is TimeSlot { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const slot = value as Partial; + return ( + typeof slot.day === "string" && + VALID_DAYS.includes(slot.day as DayCode) && + Number.isInteger(slot.startHour) && + Number.isInteger(slot.endHour) + ); +} + function createToken(byteLength: number): string { return randomBytes(byteLength).toString("base64url"); } -function tokenMatches(expected: string, received: string): boolean { - const expectedBuffer = Buffer.from(expected); - const receivedBuffer = Buffer.from(received); +function hashToken(token: string): string { + return createHash("sha256").update(token).digest("base64url"); +} + +function tokenMatches(expectedHash: string, receivedToken: string): boolean { + const expectedBuffer = Buffer.from(expectedHash, "base64url"); + const receivedBuffer = Buffer.from(hashToken(receivedToken), "base64url"); return ( expectedBuffer.length === receivedBuffer.length && timingSafeEqual(expectedBuffer, receivedBuffer) diff --git a/src/lib/scheduling/__tests__/ics-parser.test.ts b/src/lib/scheduling/__tests__/ics-parser.test.ts index cf3443a..f49f9f4 100644 --- a/src/lib/scheduling/__tests__/ics-parser.test.ts +++ b/src/lib/scheduling/__tests__/ics-parser.test.ts @@ -52,4 +52,68 @@ describe("parseIcsToSlots", () => { { day: "FRI", startHour: 0, endHour: 24 }, ]); }); + + test("빈 ICS와 VEVENT가 없는 ICS는 빈 배열을 반환한다", () => { + expect(parseIcsToSlots("")).toEqual([]); + expect(parseIcsToSlots("BEGIN:VCALENDAR\nEND:VCALENDAR")).toEqual([]); + }); + + test("DTSTART가 없거나 잘못된 VEVENT는 무시한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTEND:20260504T110000Z", + "END:VEVENT", + "BEGIN:VEVENT", + "DTSTART:not-a-date", + "DTEND:20260504T110000Z", + "END:VEVENT", + "END:VCALENDAR", + ].join("\n"); + + expect(parseIcsToSlots(ics)).toEqual([]); + }); + + test("UTC suffix가 없는 floating datetime은 조용히 제외한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART:20260504T090000", + "DTEND:20260504T110000", + "END:VEVENT", + "END:VCALENDAR", + ].join("\n"); + + expect(parseIcsToSlots(ics)).toEqual([]); + }); + + test("DTSTART와 DTEND가 같으면 슬롯을 만들지 않는다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART:20260504T090000Z", + "DTEND:20260504T090000Z", + "END:VEVENT", + "END:VCALENDAR", + ].join("\n"); + + expect(parseIcsToSlots(ics)).toEqual([]); + }); + + test("자정 시작 이벤트와 접힌 줄을 처리한다", () => { + const ics = [ + "BEGIN:VCALENDAR", + "BEGIN:VEVENT", + "DTSTART:20260504T000000Z", + "DTEND:20260504T020000Z", + "SUMMARY:접힌", + " 제목", + "END:VEVENT", + "END:VCALENDAR", + ].join("\r\n"); + + expect(parseIcsToSlots(ics)).toEqual([ + { day: "MON", startHour: 0, endHour: 2 }, + ]); + }); }); diff --git a/src/lib/scheduling/ics-parser.ts b/src/lib/scheduling/ics-parser.ts index 871b143..82b0a8c 100644 --- a/src/lib/scheduling/ics-parser.ts +++ b/src/lib/scheduling/ics-parser.ts @@ -111,7 +111,8 @@ function parseIcsDate(value: string): IcsDate | undefined { ); if (!match) return undefined; - const [, year, month, day, hour, minute, second] = match; + const [, year, month, day, hour, minute, second, utcSuffix] = match; + if (!utcSuffix) return undefined; return { date: new Date( Date.UTC( diff --git a/src/lib/supabase/__tests__/supabase.test.ts b/src/lib/supabase/__tests__/supabase.test.ts index 7ff8418..af7b88e 100644 --- a/src/lib/supabase/__tests__/supabase.test.ts +++ b/src/lib/supabase/__tests__/supabase.test.ts @@ -44,6 +44,12 @@ describe("Supabase 클라이언트 인스턴스 생성 검증", () => { expect(() => createBrowserClient()).toThrow("NEXT_PUBLIC_SUPABASE_URL"); }); + it("Supabase URL 환경변수가 공백이면 명확한 에러를 던진다", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = " "; + + expect(() => createBrowserClient()).toThrow("NEXT_PUBLIC_SUPABASE_URL"); + }); + it("Supabase anon key 환경변수가 없으면 명확한 에러를 던진다", () => { delete process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; @@ -51,4 +57,20 @@ describe("Supabase 클라이언트 인스턴스 생성 검증", () => { "NEXT_PUBLIC_SUPABASE_ANON_KEY", ); }); + + it("createServerClient도 Supabase URL 누락을 같은 계약으로 검증한다", async () => { + delete process.env.NEXT_PUBLIC_SUPABASE_URL; + + await expect(createServerClient()).rejects.toThrow( + "NEXT_PUBLIC_SUPABASE_URL", + ); + }); + + it("createServerClient도 Supabase anon key 누락을 같은 계약으로 검증한다", async () => { + delete process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + await expect(createServerClient()).rejects.toThrow( + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + ); + }); }); diff --git a/src/lib/supabase/env.ts b/src/lib/supabase/env.ts index 05e6f4f..dd67b43 100644 --- a/src/lib/supabase/env.ts +++ b/src/lib/supabase/env.ts @@ -4,8 +4,8 @@ export interface SupabaseConfig { } export function getSupabaseConfig(): SupabaseConfig { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + const url = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim(); + const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim(); if (!url) { throw new Error("NEXT_PUBLIC_SUPABASE_URL 환경변수가 설정되지 않았습니다."); diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index 312faf3..1fe3f2d 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -10,7 +10,7 @@ import { cookies } from "next/headers"; import { getSupabaseConfig } from "./env"; export async function createClient() { - const cookieStore = cookies(); + const cookieStore = await cookies(); const { url, anonKey } = getSupabaseConfig(); return createServerClient(url, anonKey, { diff --git a/tsconfig.json b/tsconfig.json index cc8ffa2..76940de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,6 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -24,16 +20,9 @@ { "name": "next" } - ] + ], + "target": "ES2017" }, - "include": [ - "next-env.d.ts", - ".next/types/**/*.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules", - "vitest.config.ts" - ] + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "vitest.config.ts"] } From 2d60858130d4a3c6c9e610fd3a9d692b88d46da7 Mon Sep 17 00:00:00 2001 From: Siul Date: Thu, 28 May 2026 14:37:46 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20npm10=20lockfile=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 75 ++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75a0778..1f776fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -141,7 +141,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1012,7 +1011,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1061,7 +1059,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -2354,7 +2351,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2484,7 +2480,6 @@ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.59.1" }, @@ -2584,6 +2579,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/@prisma/config/node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/@prisma/config/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2865,6 +2873,29 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", @@ -3090,7 +3121,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.106.1", "@supabase/functions-js": "2.106.1", @@ -3264,7 +3294,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/bcryptjs": { "version": "2.4.6", @@ -3311,7 +3342,6 @@ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3329,7 +3359,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3341,7 +3370,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3403,7 +3431,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -3949,7 +3976,6 @@ "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.7", @@ -4107,7 +4133,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4642,7 +4667,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5212,7 +5236,6 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -5622,7 +5645,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -5987,7 +6011,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6157,7 +6180,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6547,7 +6569,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7380,7 +7401,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8319,7 +8339,6 @@ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", @@ -9163,6 +9182,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10344,7 +10364,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -10567,6 +10586,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10582,6 +10602,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -10610,7 +10631,6 @@ "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" @@ -10784,7 +10804,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10797,7 +10816,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10811,7 +10829,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/read-cache": { "version": "1.0.0", @@ -12464,7 +12483,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12662,7 +12680,6 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -12756,7 +12773,6 @@ "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", @@ -13227,7 +13243,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }