Skip to content

chore: 레포지토리 위생 및 CI 검증 보강 - #32

Closed
Siul49 wants to merge 14 commits into
devfrom
feature/30-repo-hygiene-ci
Closed

chore: 레포지토리 위생 및 CI 검증 보강#32
Siul49 wants to merge 14 commits into
devfrom
feature/30-repo-hygiene-ci

Conversation

@Siul49

@Siul49 Siul49 commented May 27, 2026

Copy link
Copy Markdown
Owner

🚀 작업 내용 (What)

  • .env.claude/worktrees 추적을 제거하고, 로컬 전용 파일이 다시 올라가지 않도록 .gitignore를 보강했습니다.
  • CI를 dev/main push와 PR에서 lint, test, build, Chromium e2e까지 실행하도록 정리했습니다.
  • schedule-test 명명과 production API import를 src/lib/schedules/in-memory-store.ts로 정리하고, 오래된 스켈레톤 컴포넌트와 agent plan 문서를 제거했습니다.
  • .ics 파서를 실제 TimeSlot 변환으로 구현하고 Supabase 환경 변수 검증을 명시화했습니다.
  • 제품 문서를 docs/v1, docs/v2 구조로 정리하고 새 v2 PRD/BM/user-flow 문서를 포함했습니다.
  • npm audit fix로 강제 업그레이드 없이 가능한 dependency security patch를 lockfile에 반영했습니다.

📣 핵심 변경 이유 (Why)

  • GitHub에 올라가지 말아야 할 로컬/환경 파일과 stale 코드가 추적되어 있었고, CI가 실제 merge 전 검증 범위를 충분히 커버하지 못했습니다.
  • 레포 위생과 검증 기준을 먼저 안정화해야 이후 Supabase-backed persistence, auth, calendar 작업을 더 안전하게 이어갈 수 있습니다.
  • 남은 production audit 항목은 next/postcss 계열이며 Next 16 강제 업그레이드가 필요해 별도 이슈로 분리했습니다. Refs [SECURITY] Next.js audit 잔여 취약점 업그레이드 #31

📸 스크린샷 (Visuals, 선택)

  • UI 변경 없음

⚠️ 체크리스트 (Checklist)

  • 브랜치 컨벤션(feature/00-name)을 지켰나요?
  • 커밋 컨벤션(feat:, fix: 등)을 지켰나요?
  • 작업 전에 관련 이슈를 생성하고 연결했나요?
  • 내 코드가 팀의 기존 코드를 망가뜨리지 않았는지 확인했나요?

검증:

  • npm ci
  • npm run lint
  • npm run test (24 files / 196 tests)
  • npm run build
  • npm run test:e2e -- --project=chromium (2 passed)
  • tracked noise/stale reference 검색 완료

🔗 관련 이슈 (Issue)

Close #30
Refs #31

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json, !package-lock.json

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0cc7c103-7be7-422b-9c9c-f60abfea914d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

워크스루

Supabase 설정을 중앙화(getSupabaseConfig), Prisma로 스케줄 영속화(Schedule/Participant 모델·마이그레이션), schedules 저장소 구현(생성/조회/참여자/확정/정리), API 라우트 비동기화, ICS 파서 구현, CI·E2E·스크립트·UI 변경을 통합 적용했습니다.

변경사항

스케줄 시스템 정규화 및 데이터 계층 통합

Layer / File(s) Summary
Supabase 설정 중앙화
src/lib/supabase/env.ts, src/lib/supabase/client.ts, src/lib/supabase/server.ts, src/middleware.ts, src/lib/supabase/__tests__/supabase.test.ts
getSupabaseConfig() 추가. NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY 공백/미설정 시 에러 발생. 클라이언트·서버·미들웨어에서 해당 유틸 사용으로 통일. 환경검증 테스트 추가.
데이터베이스 스키마 및 마이그레이션
prisma/schema.prisma, prisma/migrations/20260528001000_add_schedule_persistence/migration.sql, prisma/migrations/20260528032000_add_schedule_confirmation/migration.sql, scripts/ensure-sqlite-schema.mjs, src/lib/prisma.ts
Schedule, ScheduleParticipant 모델 추가, hostTokenHash 유니크·scheduleId FK(CASCADE). SQLite 초기화 스크립트로 테이블/인덱스/열 보강. non-production에서 DATABASE_URL=file:./dev.db 기본값 설정.
Prisma 기반 스케줄 저장소
src/lib/schedules/store.ts, src/lib/schedules/__tests__/store.test.ts
스케줄 생성/공개/호스트 조회/참여자 가용성 추가/확정/전체 삭제 구현. 입력 정규화(제목·기간·요일·시간), 토큰 생성·해시·검증(timingSafeEqual), JSON 직렬화·파싱, 공통 슬롯 계산 및 검증 테스팅.
스케줄 API 라우트 마이그레이션
src/app/api/schedules/route.ts, src/app/api/schedules/[id]/route.ts, src/app/api/schedules/[id]/availability/route.ts, src/app/schedule/[id]/page.tsx
store 임포트 @/lib/schedules/store로 전환. createSchedule 사용. 라우트 params를 Promise로 받아 await params. PATCH는 try/catch로 에러 메시지에 따라 404/403/400 매핑. ScheduleParticipantPage를 async server component로 변환.
ICS 캘린더 파서 구현
src/lib/scheduling/ics-parser.ts, src/lib/scheduling/__tests__/ics-parser.test.ts
parseIcsToSlots 구현: unfoldLines, extractEvents, parseIcsDate(YYYYMMDD/ YYYYMMDDTHHMMSSZ), eventToSlot, getEndHour, toDayCode. 종일 이벤트 0~24 처리, UTC 날짜 불일치 시 end=24, 잘못된/누락 필드 무시. 다수의 유닛 테스트 추가.
CI/CD 파이프라인 강화
.github/workflows/ci.yml, .github/workflows/pr-compliance.yml, .github/workflows/issue-compliance.yml, playwright.config.ts
ci.yml: push 트리거(main/dev) 추가, permissions 제한, env 주입, npm 캐시, openssl로 JWT/ENCRYPTION_SECRET 생성, npm run db:migrate, lint/test/build, Playwright(Chromium) E2E 실행 추가. pr-compliance.yml: 템플릿 검사만 유지(기존 code-quality 제거). issue-compliance.yml: issues: read 권한 추가. Playwright 포트 동적화(PLAYWRIGHT_PORT).
저장소 정리·스크립트
.gitignore, scripts/with-database-url.mjs, scripts/ensure-sqlite-schema.mjs
.gitignore/.claude/ 추가. with-database-url.mjs CLI 래퍼(DEV DB 기본값, Windows/Unix 처리). ensure-sqlite-schema.mjs로 SQLite 테이블/인덱스/열 자동 보강.
UI/페이지·참조 컴포넌트 추가·변경
src/app/(auth)/login/page.tsx, src/app/(auth)/signup/page.tsx, src/app/calendar/connect/page.tsx, src/app/page.tsx, src/app/schedule/[id]/ScheduleRoomClient.tsx, src/app/schedule/create/CreateScheduleClient.tsx, src/components/moim/reference-ui.tsx
로그인 폼/소셜 흐름 재구성, 회원가입 페이지 추가, 캘린더 연동 페이지 추가, 홈·스케줄 룸·생성 페이지 UI 대대적 리팩터 및 참조 UI 컴포넌트(TopBar/Shell/Heatmap/Preview) 추가. QuickImport·호스트 확정 UI 흐름 반영.

시퀀스 다이어그램

sequenceDiagram
  participant Client as Browser
  participant Middleware as Next.js Middleware
  participant Route as API Route (/api/schedules)
  participant Config as getSupabaseConfig()
  participant Store as schedules/store
  participant DB as Prisma

  Client->>Middleware: request
  Middleware->>Config: getSupabaseConfig()
  Config-->>Middleware: { url, anonKey }
  Middleware->>Middleware: createServerClient(url, anonKey)

  Client->>Route: POST /api/schedules {input}
  Route->>Config: getSupabaseConfig()
  Config-->>Route: { url, anonKey }
  Route->>Store: createSchedule(input)
  Store->>Store: normalizeScheduleInput()
  Store->>Store: createToken() / hashToken()
  Store->>DB: Prisma.schedule.create(...)
  DB-->>Store: { id, hostTokenHash }
  Store-->>Route: { id, hostToken }
  Route-->>Client: { id, schedule }
Loading

예상 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~45분

Possibly related issues

Possibly related PRs

  • Siul49/moim#21 — calendar connect UI의 /api/everytime/timetable 소비와 API 구현간 계약 연관성.
  • Siul49/moim#29 — 카카오 OAuth 엔드포인트 및 인증 흐름 구현과 로그인 UI 변경의 직접적 연동.

제안 라벨

feature, test

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR 제목과 목표 범위를 벗어난 대규모 스코프 크립(UI/로그인/회원가입/캘린더 페이지 등 150+ 파일) 포함. UI/인증/캘린더 통합 작업을 별도 PR로 분리하고 이 PR을 .gitignore/CI/파서 수정만으로 제한하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 'chore:' 접두사로 시작하며 변경사항(저장소 위생 및 CI 검증)을 명확히 요약한다.
Description check ✅ Passed 설명이 4개 주요 영역(위생, CI, 코드 정리, 파서 구현)의 변경사항과 검증 내역을 관련성 있게 기술한다.
Linked Issues check ✅ Passed PR이 #30의 4개 목표(환경파일 제거, schedule-test 통합, 스켈레톤 정리, CI 보강)를 모두 충족한다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/30-repo-hygiene-ci

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/lib/schedules/__tests__/in-memory-store.test.ts (1)

93-115: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

서로 다른 실패 조건을 테스트 2개로 분리하세요.

이유: 현재 테스트는 한 케이스에서 두 번 Act/Assert를 수행해 실패 시 원인 식별이 느립니다.
방법: 빈 이름 검증과 빈 availability 검증을 개별 테스트로 분리해 AAA를 각각 독립시키세요.

정밀 수정 예시
-  test("rejects empty participant names and empty availability", () => {
+  test("rejects empty participant names", () => {
     const created = createSchedule({
       title: "스터디",
       durationMinutes: 60,
       candidateDays: ["SAT"],
       candidateStartHour: 10,
       candidateEndHour: 14,
     });

     expect(() =>
       addParticipantAvailability(created.id, {
         name: "",
         available: [{ day: "SAT", startHour: 10, endHour: 11 }],
       }),
     ).toThrow("name");
+  });
 
+  test("rejects empty availability", () => {
+    const created = createSchedule({
+      title: "스터디",
+      durationMinutes: 60,
+      candidateDays: ["SAT"],
+      candidateStartHour: 10,
+      candidateEndHour: 14,
+    });
+
     expect(() =>
       addParticipantAvailability(created.id, {
         name: "하린",
         available: [],
       }),
     ).toThrow("availability");
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schedules/__tests__/in-memory-store.test.ts` around lines 93 - 115,
Split the combined assertions into two independent tests so each follows AAA
separately: keep the same setup using createSchedule(...) but create one test
named e.g. "rejects empty participant name" that calls
addParticipantAvailability(created.id, { name: "", available: [...] }) and
asserts it throws with "name", and a second test named e.g. "rejects empty
availability" that calls addParticipantAvailability(created.id, { name: "하린",
available: [] }) and asserts it throws with "availability"; reference the
existing helpers createSchedule and addParticipantAvailability to locate and
reuse the setup.
.github/workflows/pr-compliance.yml (1)

14-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

체크리스트가 아예 없는 PR도 통과하는 허점

grep은 "- [ ]" 패턴이 존재할 때만 실패하므로, PR 템플릿을 완전히 무시하고 빈 본문이나 임의 텍스트만 작성해도 이 단계를 통과합니다 (22라인의 길이 체크 30자만 넘으면).

Why: 템플릿 준수 여부를 강제하려면 최소한 체크박스 구조가 있는지 확인해야 함.

How: "- [x]" 또는 "- [ ]" 패턴이 최소 1개 이상 있는지 먼저 검증.

🛡️ 체크리스트 존재 여부 검증 추가
       - name: 체크리스트 달성도 검사
         env:
           PR_BODY: ${{ github.event.pull_request.body }}
         run: |
+          # 체크리스트 자체가 있는지 먼저 확인
+          if ! echo "$PR_BODY" | grep -Eq "\- \[[xX \]\]"; then
+            echo "❌ PR 템플릿의 체크리스트가 누락되었습니다. 템플릿에 맞게 작성해주세요."
+            exit 1
+          fi
+
           # 체크 안 된 항목('- [ ]')이 하나라도 남아있으면 에러 뱉기
           if echo "$PR_BODY" | grep -q "\- \[ \]"; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-compliance.yml around lines 14 - 19, The workflow
currently only fails when an unchecked item "- [ ]" exists; update the shell
step that inspects PR_BODY to first verify that PR_BODY contains at least one
checkbox pattern ("- [x]" or "- [ ]") using grep on the variable PR_BODY, and if
no checkbox pattern is found emit a clear failure message asking contributors to
use the PR template and exit 1; only after that presence check, run the existing
grep for an unchecked "- [ ]" and fail if any remain. Use the same PR_BODY
variable and the existing grep logic but add the preliminary presence-check grep
and corresponding error branch before the unchecked-item check.
.github/workflows/ci.yml (1)

1-11: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

워크플로우 권한을 명시적으로 제한하면 보안 강화

워크플로우가 기본 권한(read-all)을 사용하고 있습니다. 명시적으로 최소 권한만 부여하면 공급망 공격 시 영향 범위를 줄일 수 있습니다.

Why: GitHub Actions의 GITHUB_TOKEN은 기본적으로 repository contents에 쓰기 권한이 있어, 악의적 의존성이 코드를 변조할 여지가 있음.

How: 워크플로우 최상단에 permissions 블록 추가.

🔒 권한 제한 예시
 name: CI Pipeline
 
+permissions:
+  contents: read
+
 on:
   pull_request:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 1 - 11, Add a top-level GitHub Actions
permissions block to the CI Pipeline workflow (the file starting with "name: CI
Pipeline") to explicitly restrict GITHUB_TOKEN rights instead of using the
default read-all; identify the minimal permissions your jobs need (e.g.,
contents: read, packages: read, actions: read, id-token: write) and add a
top-level permissions: { ... } stanza above the on: section so the workflow
grants only those specific permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 20-21: The workflow hardcodes weak CI secrets (JWT_SECRET and
ENCRYPTION_SECRET); replace these with stronger CI-only secrets and/or generate
them at runtime: add repository secrets (e.g., CI_JWT_SECRET and
CI_ENCRYPTION_SECRET) and reference those in the workflow or use an actions step
to generate secure random values (openssl rand -base64 32 for JWT, openssl rand
-hex 32 for the 64-char encryption key) and set them into the environment
variables JWT_SECRET and ENCRYPTION_SECRET before running tests to ensure CI
exercises real cryptographic conditions.

In @.github/workflows/pr-compliance.yml:
- Around line 1-7: Add an explicit permissions block at the top of the GitHub
Actions workflow (the YAML that starts with "name: PR 템플릿 검사") to restrict
permissions to the minimum required; specifically add a top-level permissions:
pull-requests: read entry so the workflow only has read access to pull request
data instead of default broader permissions.

In `@src/lib/scheduling/__tests__/ics-parser.test.ts`:
- Around line 1-55: Add edge-case unit tests around parseIcsToSlots to cover
malformed and boundary inputs: add tests that ensure VEVENTs without DTSTART are
ignored, invalid DTSTART formats result in no slots, empty ICS string and
VCALENDAR-without-VEVENT return [], events where DTSTART === DTEND are excluded,
midnight-start events (DTSTART:...T000000Z) are parsed as startHour 0, and
folded lines are properly unfolded before parsing; add these tests in
src/lib/scheduling/__tests__/ics-parser.test.ts alongside the existing suites to
validate parseIcsToSlots behavior for robustness.

In `@src/lib/scheduling/ics-parser.ts`:
- Around line 109-127: parseIcsDate currently treats all datetimes as UTC;
change it so datetimes without a trailing "Z" are explicitly excluded by
returning undefined: inside parseIcsDate (the regex match block), after
obtaining match and before constructing the Date, check the captured Z group
(match[7] or the destructured variable) and if it is falsy return undefined;
only call Date.UTC and return the { date: ..., isDateOnly: false } object when
the Z suffix is present. This keeps existing behavior for Z-suffixed UTC
datetimes while quietly excluding floating/local datetimes per the recommended
approach.

In `@src/lib/supabase/__tests__/supabase.test.ts`:
- Around line 41-53: Add the same negative tests you wrote for
createBrowserClient to cover server-side usage: for each env var case (missing
NEXT_PUBLIC_SUPABASE_URL and missing NEXT_PUBLIC_SUPABASE_ANON_KEY) arrange by
deleting the env var, act by calling createServerClient(), and assert that it
throws the same descriptive error (matching the env var name); this ensures
getSupabaseConfig() contract is protected for browser and server paths and keeps
the AAA structure in the tests.

In `@src/lib/supabase/env.ts`:
- Around line 10-18: The current checks for NEXT_PUBLIC_SUPABASE_URL and
NEXT_PUBLIC_SUPABASE_ANON_KEY only test falsiness and allow whitespace-only
values; update the validation in the env handling (the variables/url and anonKey
checks in src/lib/supabase/env.ts) to trim() the values and throw the same Error
when url.trim() === "" or anonKey.trim() === "" so whitespace-only env vars are
treated as missing; ensure you call trim() on the exact variables used to create
the Supabase client so the client creation never proceeds with blank strings.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 1-11: Add a top-level GitHub Actions permissions block to the CI
Pipeline workflow (the file starting with "name: CI Pipeline") to explicitly
restrict GITHUB_TOKEN rights instead of using the default read-all; identify the
minimal permissions your jobs need (e.g., contents: read, packages: read,
actions: read, id-token: write) and add a top-level permissions: { ... } stanza
above the on: section so the workflow grants only those specific permissions.

In @.github/workflows/pr-compliance.yml:
- Around line 14-19: The workflow currently only fails when an unchecked item "-
[ ]" exists; update the shell step that inspects PR_BODY to first verify that
PR_BODY contains at least one checkbox pattern ("- [x]" or "- [ ]") using grep
on the variable PR_BODY, and if no checkbox pattern is found emit a clear
failure message asking contributors to use the PR template and exit 1; only
after that presence check, run the existing grep for an unchecked "- [ ]" and
fail if any remain. Use the same PR_BODY variable and the existing grep logic
but add the preliminary presence-check grep and corresponding error branch
before the unchecked-item check.

In `@src/lib/schedules/__tests__/in-memory-store.test.ts`:
- Around line 93-115: Split the combined assertions into two independent tests
so each follows AAA separately: keep the same setup using createSchedule(...)
but create one test named e.g. "rejects empty participant name" that calls
addParticipantAvailability(created.id, { name: "", available: [...] }) and
asserts it throws with "name", and a second test named e.g. "rejects empty
availability" that calls addParticipantAvailability(created.id, { name: "하린",
available: [] }) and asserts it throws with "availability"; reference the
existing helpers createSchedule and addParticipantAvailability to locate and
reuse the setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c0b0965f-02e1-41ac-ba6e-a7e9e23bb286

📥 Commits

Reviewing files that changed from the base of the PR and between e40c252 and 4e20499.

⛔ Files ignored due to path filters (9)
  • docs/codex-work-context.md is excluded by !**/*.md
  • docs/superpowers/plans/2026-04-27-moim-user-test-prototype.md is excluded by !**/*.md
  • docs/v1/codex-work-context.md is excluded by !**/*.md
  • docs/v1/provided-documents-summary.md is excluded by !**/*.md
  • docs/v1/user-flow.md is excluded by !**/*.md
  • docs/v2/monetization-strategy.md is excluded by !**/*.md
  • docs/v2/prd.md is excluded by !**/*.md
  • docs/v2/user-flow.md is excluded by !**/*.md
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json, !package-lock.json
📒 Files selected for processing (20)
  • .claude/worktrees/availability-aggregation
  • .env
  • .github/workflows/ci.yml
  • .github/workflows/pr-compliance.yml
  • .gitignore
  • src/app/api/schedules/[id]/availability/route.ts
  • src/app/api/schedules/[id]/route.ts
  • src/app/api/schedules/route.ts
  • src/components/schedule/AvailabilityResult.tsx
  • src/components/schedule/ParticipantList.tsx
  • src/components/schedule/TimeGrid.tsx
  • src/lib/schedules/__tests__/in-memory-store.test.ts
  • src/lib/schedules/in-memory-store.ts
  • src/lib/scheduling/__tests__/ics-parser.test.ts
  • src/lib/scheduling/ics-parser.ts
  • src/lib/supabase/__tests__/supabase.test.ts
  • src/lib/supabase/client.ts
  • src/lib/supabase/env.ts
  • src/lib/supabase/server.ts
  • src/middleware.ts
💤 Files with no reviewable changes (5)
  • .claude/worktrees/availability-aggregation
  • src/components/schedule/TimeGrid.tsx
  • src/components/schedule/AvailabilityResult.tsx
  • .env
  • src/components/schedule/ParticipantList.tsx

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/pr-compliance.yml
Comment thread src/lib/scheduling/__tests__/ics-parser.test.ts
Comment on lines +84 to +93
function getEndHour(start: IcsDate, end: IcsDate): number {
if (end.isDateOnly) return 24;

const sameUtcDay =
start.date.getUTCFullYear() === end.date.getUTCFullYear() &&
start.date.getUTCMonth() === end.date.getUTCMonth() &&
start.date.getUTCDate() === end.date.getUTCDate();

return sameUtcDay ? end.date.getUTCHours() : 24;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

여러 날 걸친 이벤트의 일부 시간대가 조용히 제외됨.

getEndHourDTSTARTDTEND가 다른 UTC 날짜일 경우 무조건 24를 반환한다(line 92). 예를 들어 "5월 4일 23:00 ~ 5월 5일 02:00" 이벤트는 "MON 23-24" 슬롯만 생성되고, 5월 5일 0-2시는 버려진다.

Why: 사용자의 실제 busy time이 누락되어 일정 조율 시 충돌이 발생할 수 있다.

How: 다음 중 하나를 선택:

  1. (단기) 여러 날 걸친 이벤트를 감지하여 해당 이벤트를 제외하고 콘솔 경고를 출력한다.
  2. (중기) TODO 주석으로 향후 multi-day 확장 계획을 명시한다.
♻️ 단기 수정안: 여러 날 이벤트 명시적 제외
 function eventToSlot(event: IcsEvent): TimeSlot | null {
   if (!event.start || !event.end) return null;
 
   const { start, end } = event;
+  
+  // TODO(`#XX`): 여러 날에 걸친 이벤트는 현재 MVP 범위 밖. 향후 확장 필요.
+  const spansDays =
+    start.date.getUTCFullYear() !== end.date.getUTCFullYear() ||
+    start.date.getUTCMonth() !== end.date.getUTCMonth() ||
+    start.date.getUTCDate() !== end.date.getUTCDate();
+  
+  if (spansDays && !start.isDateOnly && !end.isDateOnly) {
+    // 시간 이벤트가 여러 날 걸치면 제외
+    return null;
+  }
+  
   const startHour = start.isDateOnly ? 0 : start.date.getUTCHours();
   const endHour = getEndHour(start, end);
 
   if (startHour >= endHour) return null;
 
   return {
     day: toDayCode(start.date.getUTCDay()),
     startHour,
     endHour,
   };
 }

Comment thread src/lib/scheduling/ics-parser.ts
Comment thread src/lib/supabase/__tests__/supabase.test.ts
Comment thread src/lib/supabase/env.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
playwright.config.ts (1)

28-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

playwright.config.ts에 firefox 프로젝트를 추가하세요.

Why: projectschromium, webkit, Mobile Safari만 포함되어 Firefox(Gecko) 회귀 탐지가 누락됩니다.
How: projectsfirefox를 최소 1개 추가하세요.

최소 수정 예시
   projects: [
     {
       name: "chromium",
       use: { ...devices["Desktop Chrome"] },
     },
+    {
+      name: "firefox",
+      use: { ...devices["Desktop Firefox"] },
+    },
     {
       name: "webkit",
       use: { ...devices["Desktop Safari"] },
     },
     {
       name: "Mobile Safari",
       use: { ...devices["iPhone 13"] },
     },
   ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright.config.ts` around lines 28 - 41, Add a Firefox project entry to
the existing projects array in playwright.config.ts so regressions run against
Gecko; locate the projects array that currently lists name: "chromium", name:
"webkit", and name: "Mobile Safari" and add a new object with name: "firefox"
and use: { ...devices["Desktop Firefox"] } (or the appropriate Desktop Firefox
device) ensuring the new entry matches the existing project shape.
♻️ Duplicate comments (1)
src/lib/scheduling/__tests__/ics-parser.test.ts (1)

103-118: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

라인 폴딩 테스트가 핵심 파싱 경로를 직접 검증하지 않습니다.

Why: 현재는 SUMMARY만 접어서 unfoldLinesDTSTART/DTEND 파싱 정확도에 미치는 영향을 검증하지 못합니다. 회귀가 나도 테스트가 통과할 수 있습니다.
How: 접힘 대상을 DTSTART(또는 DTEND)로 바꿔 실제 시간값 복원을 검증하세요.

최소 수정 예시
   test("자정 시작 이벤트와 접힌 줄을 처리한다", () => {
     const ics = [
       "BEGIN:VCALENDAR",
       "BEGIN:VEVENT",
-      "DTSTART:20260504T000000Z",
+      "DTSTART:20260504T00",
+      " 0000Z",
       "DTEND:20260504T020000Z",
-      "SUMMARY:접힌",
-      " 제목",
       "END:VEVENT",
       "END:VCALENDAR",
     ].join("\r\n");

     expect(parseIcsToSlots(ics)).toEqual([
       { day: "MON", startHour: 0, endHour: 2 },
     ]);
   });

As per coding guidelines **/__tests__/**: "경계값, 에러 케이스, 빈 입력 등 엣지 케이스 커버리지를 평가"하고 "테스트 설명이 행동(behavior)을 명확히 기술"해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/scheduling/__tests__/ics-parser.test.ts` around lines 103 - 118, The
test currently folds only SUMMARY so it doesn't verify that unfoldLines
correctly reconstructs folded DTSTART/DTEND values; update the test for
parseIcsToSlots to fold a DTSTART (or DTEND) line across two physical lines
(e.g. "DTSTART:2026050" newline "4T000000Z") and assert the parsed slot still
yields { day: "MON", startHour: 0, endHour: 2 }; ensure the test description
mentions folded DTSTART and confirm parseIcsToSlots (which relies on
unfoldLines) restores the correct timestamp values for parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 30-34: The workflow uses floating action tags (actions/checkout@v6
and actions/setup-node@v6) and misses the checkout credential safeguard; update
the Checkout Repository step (uses: actions/checkout@v6) to pin to the action's
exact 40-hex commit SHA and add with: persist-credentials: false, and likewise
pin the Setup Node.js step (uses: actions/setup-node@v6) to its 40-hex commit
SHA to ensure reproducibility and prevent credential persistence.

In `@scripts/ensure-sqlite-schema.mjs`:
- Around line 7-83: The script does not guarantee Prisma is disconnected if any
prisma.$executeRawUnsafe(...) throws; wrap the DDL/index execution calls in a
try/finally so prisma.$disconnect() is always awaited in the finally block.
Specifically, create a try { /* all calls to prisma.$executeRawUnsafe(...) */ }
finally { await prisma.$disconnect(); } around the sequence that currently calls
prisma.$executeRawUnsafe and remove the dangling final await
prisma.$disconnect() outside that block to ensure deterministic cleanup.

In `@scripts/with-database-url.mjs`:
- Around line 5-41: Change scripts/with-database-url.mjs so DATABASE_URL is only
defaulted in non-production like other modules: wrap the existing
process.env.DATABASE_URL ||= "file:./dev.db"; behind a NODE_ENV check (use
process.env.NODE_ENV !== "production") to avoid forcing SQLite in production;
also attach an error handler to the spawned child (the variable child created
from spawn) by adding child.on("error", ...) that logs the error (e.g., `Command
failed to start: ${error.message}`) and exits with code 1 so process spawn
failures are reported and halt consistently with the exit handler.

In `@src/lib/schedules/store.ts`:
- Around line 143-146: clearSchedules currently runs two separate deleteMany
calls (prisma.scheduleParticipant.deleteMany and prisma.schedule.deleteMany)
which can leave the DB in an inconsistent state if the second call fails; wrap
both deletions in a single atomic transaction (using prisma.$transaction or the
transaction API) so that either both participant rows and schedules are deleted
or neither are, ensuring rollback on error and preserving test teardown
atomicity.

In `@src/lib/supabase/__tests__/supabase.test.ts`:
- Around line 47-51: Add symmetric boundary tests for the anon key blank case:
set process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "   " and assert the same error
is thrown as for the URL so the contract matches env.ts trimming. Concretely, in
the existing test file add one test that uses createBrowserClient() and expects
a throw mentioning "NEXT_PUBLIC_SUPABASE_ANON_KEY" and a corresponding test for
the server path (the test exercising the server-side client creation, e.g.,
createServerClient or the server-side factory used in lines ~61-75) with the
same expectation.

---

Outside diff comments:
In `@playwright.config.ts`:
- Around line 28-41: Add a Firefox project entry to the existing projects array
in playwright.config.ts so regressions run against Gecko; locate the projects
array that currently lists name: "chromium", name: "webkit", and name: "Mobile
Safari" and add a new object with name: "firefox" and use: { ...devices["Desktop
Firefox"] } (or the appropriate Desktop Firefox device) ensuring the new entry
matches the existing project shape.

---

Duplicate comments:
In `@src/lib/scheduling/__tests__/ics-parser.test.ts`:
- Around line 103-118: The test currently folds only SUMMARY so it doesn't
verify that unfoldLines correctly reconstructs folded DTSTART/DTEND values;
update the test for parseIcsToSlots to fold a DTSTART (or DTEND) line across two
physical lines (e.g. "DTSTART:2026050" newline "4T000000Z") and assert the
parsed slot still yields { day: "MON", startHour: 0, endHour: 2 }; ensure the
test description mentions folded DTSTART and confirm parseIcsToSlots (which
relies on unfoldLines) restores the correct timestamp values for parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 630fc1d0-0b73-4e30-81b7-a9c1bb6aa0d4

📥 Commits

Reviewing files that changed from the base of the PR and between 4e20499 and e632bc6.

⛔ Files ignored due to path filters (9)
  • ARCHITECTURE.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • convention.md is excluded by !**/*.md
  • docs/README.md is excluded by !**/*.md
  • docs/v1/codex-work-context.md is excluded by !**/*.md
  • docs/v1/provided-documents-summary.md is excluded by !**/*.md
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json, !package-lock.json
  • package.json is excluded by !**/*.json
  • tsconfig.json is excluded by !**/*.json
📒 Files selected for processing (20)
  • .github/workflows/ci.yml
  • .github/workflows/issue-compliance.yml
  • .github/workflows/pr-compliance.yml
  • playwright.config.ts
  • prisma/migrations/20260528001000_add_schedule_persistence/migration.sql
  • prisma/schema.prisma
  • scripts/ensure-sqlite-schema.mjs
  • scripts/with-database-url.mjs
  • src/app/api/schedules/[id]/availability/route.ts
  • src/app/api/schedules/[id]/route.ts
  • src/app/api/schedules/route.ts
  • src/app/schedule/[id]/page.tsx
  • src/lib/prisma.ts
  • src/lib/schedules/__tests__/store.test.ts
  • src/lib/schedules/store.ts
  • src/lib/scheduling/__tests__/ics-parser.test.ts
  • src/lib/scheduling/ics-parser.ts
  • src/lib/supabase/__tests__/supabase.test.ts
  • src/lib/supabase/env.ts
  • src/lib/supabase/server.ts

Comment thread .github/workflows/ci.yml
Comment on lines 30 to +34
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/ci.yml"

echo "[검증1] actions 참조가 SHA(40 hex)로 고정됐는지 확인"
python - <<'PY'
import re, pathlib
text = pathlib.Path(".github/workflows/ci.yml").read_text()
for action in ("checkout", "setup-node"):
    m = re.search(rf'uses:\s*actions/{action}@([^\n]+)', text)
    ref = m.group(1).strip() if m else None
    ok = bool(ref and re.fullmatch(r"[0-9a-f]{40}", ref))
    print(f"{action}: ref={ref!r}, sha_pinned={ok}")
PY

echo "[검증2] checkout persist-credentials: false 존재 여부"
rg -n 'persist-credentials:\s*false' "$file" || true

Repository: Siul49/moim

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/ci.yml"

echo "=== Relevant checkout/setup-node steps (lines ~1-120) ==="
nl -ba "$file" | sed -n '1,120p'

echo "=== Exact checkout uses line and any adjacent with/params ==="
nl -ba "$file" | sed -n '/actions\/checkout@/ {p; n; p; n; p; n; p; }'

Repository: Siul49/moim

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/ci.yml"

echo "=== Relevant checkout/setup-node steps (first 120 lines) ==="
sed -n '1,120p' "$file"

echo "=== Exact lines containing actions/checkout@ and actions/setup-node@ ==="
rg -n 'actions/(checkout|setup-node)@' "$file"

echo "=== Any persist-credentials under checkout step ==="
rg -n --context=3 'actions/checkout@' "$file" || true
rg -n 'persist-credentials:' "$file" || true

Repository: Siul49/moim

Length of output: 2128


🌐 Web query:

actions/checkout persist-credentials default value true

💡 Result:

The actions/checkout input persist-credentials defaults to true (i.e., credentials/token are persisted in the local git config unless you set persist-credentials: false). [1][2] Source detail: the actions/checkout documentation states the auth token is persisted in the local git config to enable authenticated git commands, and that you can opt out by setting persist-credentials: false; it also lists “Default: true” for persist-credentials in the action inputs section. [1][2]

Citations:


CI 공급망/자격증명 노출: actions 참조 SHA 고정 + checkout persist-credentials 차단 필요

  • .github/workflows/ci.yml에서 actions/checkout@v6, actions/setup-node@v6가 SHA로 미고정되어 태그 재지정 시 파이프라인 무결성/재현성이 약해집니다(라인 31, 34).
  • actions/checkoutpersist-credentials: false가 없어서 persist-credentials 기본값(true)으로 토큰이 로컬 git config에 남을 수 있습니다.

Why: CI 실행 환경에서 미고정 액션/자격증명 영속은 공급망·자격증명 노출 표면을 키웁니다.
How: uses:는 40-hex 커밋 SHA로 고정하고, checkout step에 with: persist-credentials: false를 명시하세요.

최소 수정 예시
-      - name: 저장소 체크아웃 (Checkout Repository)
-        uses: actions/checkout@v6
+      - name: 저장소 체크아웃 (Checkout Repository)
+        uses: actions/checkout@<검증된_커밋_SHA>
+        with:
+          persist-credentials: false

-      - name: Node.js 세팅 (Setup Node.js)
-        uses: actions/setup-node@v6
+      - name: Node.js 세팅 (Setup Node.js)
+        uses: actions/setup-node@<검증된_커밋_SHA>
         with:
           node-version: "20"
           cache: "npm"
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 30-31: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 31-31: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 34-34: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 30 - 34, The workflow uses floating
action tags (actions/checkout@v6 and actions/setup-node@v6) and misses the
checkout credential safeguard; update the Checkout Repository step (uses:
actions/checkout@v6) to pin to the action's exact 40-hex commit SHA and add
with: persist-credentials: false, and likewise pin the Setup Node.js step (uses:
actions/setup-node@v6) to its 40-hex commit SHA to ensure reproducibility and
prevent credential persistence.

Comment on lines +7 to +83
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la
fd -a "ensure-sqlite-schema.mjs" . || true
# Print file with line numbers (limit)
FILE="$(fd -a "ensure-sqlite-schema.mjs" . | head -n 1)"
echo "FOUND: $FILE"
wc -l "$FILE"
sed -n '1,160p' "$FILE" | cat -n

Repository: Siul49/moim

Length of output: 5608


실패 경로에서 Prisma 연결 해제가 보장되지 않습니다 (try/finally 필요)

Why: scripts/ensure-sqlite-schema.mjs에서 중간의 await prisma.$executeRawUnsafe(...)가 하나라도 실패하면, 마지막 83라인의 await prisma.$disconnect()까지 도달하지 못합니다.

How: DDL/인덱스 실행 구간을 try/finally로 감싸고 finally에서 await prisma.$disconnect()가 항상 실행되게 하세요.

최소 수정 예시
 const prisma = new PrismaClient();
 
-await prisma.$executeRawUnsafe(`
-  CREATE TABLE IF NOT EXISTS "User" (
-    ...
-`);
-
-await prisma.$executeRawUnsafe(`
-  CREATE TABLE IF NOT EXISTS "SocialAccount" (
-    ...
-`);
-
-// (나머지 DDL/인덱스 생성 구문 유지)
-
-await prisma.$disconnect();
+try {
+  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
+    )
+  `);
+
+  // (나머지 DDL/인덱스 생성 구문 유지)
+} finally {
+  await prisma.$disconnect();
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ensure-sqlite-schema.mjs` around lines 7 - 83, The script does not
guarantee Prisma is disconnected if any prisma.$executeRawUnsafe(...) throws;
wrap the DDL/index execution calls in a try/finally so prisma.$disconnect() is
always awaited in the finally block. Specifically, create a try { /* all calls
to prisma.$executeRawUnsafe(...) */ } finally { await prisma.$disconnect(); }
around the sequence that currently calls prisma.$executeRawUnsafe and remove the
dangling final await prisma.$disconnect() outside that block to ensure
deterministic cleanup.

Comment on lines +5 to +41
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 <command> [...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);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la scripts || true
sed -n '1,120p' scripts/with-database-url.mjs

Repository: Siul49/moim

Length of output: 1431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la scripts || true
sed -n '1,120p' scripts/with-database-url.mjs

Repository: Siul49/moim

Length of output: 1431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 관련 호출/환경변수 사용처 확인
ls -ლა
test -f package.json && sed -n '1,200p' package.json || true
rg -n "with-database-url\.mjs|DATABASE_URL|NODE_ENV" -S . || true

Repository: Siul49/moim

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 관련 호출/환경변수 사용처 확인
test -f package.json && sed -n '1,220p' package.json || true
rg -n "with-database-url\.mjs|DATABASE_URL|NODE_ENV" -S . || true

Repository: Siul49/moim

Length of output: 3796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' scripts/ensure-sqlite-schema.mjs
sed -n '1,80p' src/lib/prisma.ts
sed -n '1,80p' README.md
sed -n '1,120p' scripts/with-database-url.mjs

Repository: Siul49/moim

Length of output: 5312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' scripts/ensure-sqlite-schema.mjs
sed -n '1,80p' src/lib/prisma.ts
sed -n '1,80p' README.md
sed -n '1,120p' scripts/with-database-url.mjs

Repository: Siul49/moim

Length of output: 5312


🏁 Script executed:

rg -n "with-database-url\.mjs" -S . || true
rg -n "child\.on\\(\"error\"\\)|\\.on\\(\"error\"\\)" scripts/with-database-url\.mjs -S || true

Repository: Siul49/moim

Length of output: 506


🏁 Script executed:

rg -n "with-database-url\.mjs" -S . || true
rg -n "child\.on\\(\"error\"\\)|\\.on\\(\"error\"\\)" scripts/with-database-url\.mjs -S || true

Repository: Siul49/moim

Length of output: 506


DATABASE_URL 기본값 강제 범위 조정 및 spawn error 핸들러 추가

  • scripts/with-database-url.mjsprocess.env.DATABASE_URLNODE_ENV와 무관하게 file:./dev.db로 강제합니다. 반면 src/lib/prisma.ts, scripts/ensure-sqlite-schema.mjsNODE_ENV !== "production"에서만 기본값을 둬 정책이 어긋납니다. 비테스트/프로덕션 성격 실행 시 의도치 않은 SQLite 연결 위험이 있습니다.

    • How:
    if (process.env.NODE_ENV !== "production") {
      process.env.DATABASE_URL ||= "file:./dev.db";
    }
  • 자식 프로세스 스폰 실패는 exit이 아니라 childerror 이벤트로 들어옵니다. 현재는 exit만 처리해 시작 실패 시 메시지가 빈약하거나 비정상 종료로 이어질 수 있습니다.

    • How:
    child.on("error", (error) => {
      console.error(`Command failed to start: ${error.message}`);
      process.exit(1);
    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/with-database-url.mjs` around lines 5 - 41, Change
scripts/with-database-url.mjs so DATABASE_URL is only defaulted in
non-production like other modules: wrap the existing process.env.DATABASE_URL
||= "file:./dev.db"; behind a NODE_ENV check (use process.env.NODE_ENV !==
"production") to avoid forcing SQLite in production; also attach an error
handler to the spawned child (the variable child created from spawn) by adding
child.on("error", ...) that logs the error (e.g., `Command failed to start:
${error.message}`) and exits with code 1 so process spawn failures are reported
and halt consistently with the exit handler.

Comment on lines +143 to +146
export async function clearSchedules() {
await prisma.scheduleParticipant.deleteMany();
await prisma.schedule.deleteMany();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

clearSchedules에 트랜잭션 래핑 필요

deleteMany 호출 사이에 오류 발생 시 participants는 삭제되고 schedule은 남는 불일치 상태 가능. 테스트 정리용이라도 원자성 보장 필요.

♻️ 트랜잭션 적용
 export async function clearSchedules() {
-  await prisma.scheduleParticipant.deleteMany();
-  await prisma.schedule.deleteMany();
+  await prisma.$transaction([
+    prisma.scheduleParticipant.deleteMany(),
+    prisma.schedule.deleteMany(),
+  ]);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schedules/store.ts` around lines 143 - 146, clearSchedules currently
runs two separate deleteMany calls (prisma.scheduleParticipant.deleteMany and
prisma.schedule.deleteMany) which can leave the DB in an inconsistent state if
the second call fails; wrap both deletions in a single atomic transaction (using
prisma.$transaction or the transaction API) so that either both participant rows
and schedules are deleted or neither are, ensuring rollback on error and
preserving test teardown atomicity.

Comment on lines +47 to +51
it("Supabase URL 환경변수가 공백이면 명확한 에러를 던진다", () => {
process.env.NEXT_PUBLIC_SUPABASE_URL = " ";

expect(() => createBrowserClient()).toThrow("NEXT_PUBLIC_SUPABASE_URL");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

anon key 공백 경계값 검증이 빠져 계약 테스트가 비대칭입니다.

근거: Line 47-51에서 URL 공백은 검증하지만, NEXT_PUBLIC_SUPABASE_ANON_KEY 공백(" ")은 브라우저/서버 경로 모두 테스트가 없습니다. env.ts는 두 키를 동일하게 trim() 검증하므로 동일 경계값을 맞춰야 회귀를 막습니다.
수정: anon key 공백 케이스를 브라우저/서버에 각각 1개씩 추가하세요.

최소 수정 예시
+  it("Supabase anon key 환경변수가 공백이면 명확한 에러를 던진다", () => {
+    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "   ";
+    expect(() => createBrowserClient()).toThrow(
+      "NEXT_PUBLIC_SUPABASE_ANON_KEY",
+    );
+  });
+
+  it("createServerClient도 Supabase anon key 공백을 같은 계약으로 검증한다", async () => {
+    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "   ";
+    await expect(createServerClient()).rejects.toThrow(
+      "NEXT_PUBLIC_SUPABASE_ANON_KEY",
+    );
+  });

Also applies to: 61-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/supabase/__tests__/supabase.test.ts` around lines 47 - 51, Add
symmetric boundary tests for the anon key blank case: set
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "   " and assert the same error is
thrown as for the URL so the contract matches env.ts trimming. Concretely, in
the existing test file add one test that uses createBrowserClient() and expects
a throw mentioning "NEXT_PUBLIC_SUPABASE_ANON_KEY" and a corresponding test for
the server path (the test exercising the server-side client creation, e.g.,
createServerClient or the server-side factory used in lines ~61-75) with the
same expectation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

♻️ Duplicate comments (2)
src/lib/schedules/store.ts (1)

173-176: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

clearSchedules에 트랜잭션 래핑 필요

기존 리뷰 지적 여전히 유효. 두 deleteMany 사이 오류 시 불일치 상태 가능.

Why: 테스트 정리용이라도 원자성 보장 필요.

How:

 export async function clearSchedules() {
-  await prisma.scheduleParticipant.deleteMany();
-  await prisma.schedule.deleteMany();
+  await prisma.$transaction([
+    prisma.scheduleParticipant.deleteMany(),
+    prisma.schedule.deleteMany(),
+  ]);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schedules/store.ts` around lines 173 - 176, The clearSchedules
function must perform both deletions atomically to avoid inconsistent state if
an error occurs between them; replace the two separate calls to
prisma.scheduleParticipant.deleteMany and prisma.schedule.deleteMany with a
single Prisma transaction (e.g., use prisma.$transaction) that runs both deletes
(participants first, then schedules) within the same transaction scope so either
both succeed or both roll back.
scripts/ensure-sqlite-schema.mjs (1)

7-102: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

실패 경로에서 Prisma 연결 해제 미보장 (try/finally 필요)

기존 리뷰에서 지적된 문제 여전히 존재. DDL 실행 중 예외 발생 시 prisma.$disconnect() 미호출.

Why: 커넥션 누수, 리소스 정리 실패.

How: 전체 DDL 구간을 try/finally로 감싸기.

♻️ try/finally 적용
 const prisma = new PrismaClient();

+try {
 await prisma.$executeRawUnsafe(`
   CREATE TABLE IF NOT EXISTS "User" (
     ...
 `);

 // ... 나머지 DDL/인덱스 생성

 const scheduleColumns = await prisma.$queryRawUnsafe(
   `PRAGMA table_info("Schedule")`,
 );
 // ... ALTER TABLE 로직

+} finally {
-await prisma.$disconnect();
+  await prisma.$disconnect();
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ensure-sqlite-schema.mjs` around lines 7 - 102, Wrap all DDL
operations (calls to prisma.$executeRawUnsafe, prisma.$queryRawUnsafe and the
scheduleColumns logic) in a try/finally so prisma.$disconnect() is always
called; i.e., open a try before the first prisma.$executeRawUnsafe, perform the
CREATE TABLE/INDEX/PRAGMA/ALTER operations inside the try, and move the
prisma.$disconnect() into the finally block to ensure it runs even if any
prisma.$executeRawUnsafe or prisma.$queryRawUnsafe throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/participant-flow.spec.ts`:
- Around line 37-38: The test currently uses page.getByRole("button", { name: "이
시간 확정" }).first().click(), which is flaky when multiple same-label buttons
exist; instead locate the specific slot container (e.g., use
page.getByText("<slot label/text>").locator or page.locator for the slot
element) and call locator.getByRole("button", { name: "이 시간 확정" }).click()
within that slot context so the click targets the intended slot button; keep the
subsequent expect(page.getByText("시간이 확정되었습니다")).toBeVisible() assertion
unchanged.

In `@src/app/`(auth)/login/page.tsx:
- Around line 92-94: The "비밀번호 찾기" Link currently points to the same page (Link
with href="/login"); update it to the intended password-recovery route (e.g.,
change href to "/forgot-password" or "/reset-password") or, if the feature isn't
implemented yet, disable the link by replacing it with a non-interactive element
or add aria-disabled and preventDefault on click so it doesn't navigate back to
the login page; ensure you modify the Link element's href and/or element type in
page.tsx accordingly.

In `@src/app/`(auth)/signup/page.tsx:
- Around line 40-43: Before sending the signup request, enforce that all
required agreement checkboxes are checked and mark the checkbox inputs as
required: in the signup page's submit handler (the function that currently
checks form.password and form.passwordConfirm and then calls
fetch("/api/auth/signup")), add validation that verifies the required agreement
flags on the form state (e.g., termsAccepted, privacyAccepted) are true and
throw a user-facing error if not; also update the checkbox inputs for the
mandatory agreements (the JSX inputs rendered around the areas referenced
145-166) to include the HTML required attribute so browsers enforce them too.
Ensure the validation occurs prior to the password-confirm check and the fetch
call so no request is sent when required agreements are unchecked.
- Around line 63-299: The page uses hardcoded HEX colors throughout; replace
these with Tailwind token classes from globals.css and migrate common UI bits to
shadcn/ui components: swap direct hex classes (e.g., "text-[`#6252ac`]",
"bg-[`#8f7bd6`]", "border-[`#dedbe3`]", "text-[`#6f6a73`]", etc.) in the render of the
default export and in components SocialButton, TextField, PasswordField, and
usage of PurpleButton/CheckCircle2 to the corresponding design-token classes
(e.g., text-primary, bg-primary-500, border-muted, text-secondary) defined in
src/app/globals.css; extract shared button/input styles into shadcn/ui
Button/Input variants and use those components instead of raw <button>/<input>
where possible, ensuring autoComplete/props remain unchanged and preserving
className merges for dark variants in SocialButton.

In `@src/app/api/schedules/`[id]/route.ts:
- Around line 37-60: The PATCH handler calls confirmSchedule(id, body.hostToken,
body.confirmedSlot) without validating request body, which can throw from the
store layer; add a simple type guard after reading body in PATCH to check that
body.hostToken is a non-empty string and body.confirmedSlot has the expected
shape/type (e.g., non-null object or specific primitive) and if validation fails
return NextResponse.json({ error: "invalid request body" }, { status: 400 })
instead of calling confirmSchedule; update references in this function (PATCH,
params, body.hostToken, body.confirmedSlot, confirmSchedule) only—no store calls
should run when validation fails.

In `@src/app/calendar/connect/page.tsx`:
- Around line 53-63: In submitEverytimeFile, add client-side prevalidation to
immediately reject non-ICS or oversized files before building FormData: check
the File MIME type (e.g., text/calendar or application/octet-stream) and/or
file.name extension endsWith(".ics"), and enforce a maxSize (e.g., 5MB) so that
invalid files return early — set an error message via setMessage(...) and clear
loading state setIsLoading("") (and return) when validation fails; apply the
same validation logic to the other upload handler referenced around lines
206-213 to keep behavior consistent.
- Around line 107-307: This file uses many hard-coded HEX Tailwind classes
(e.g., text-[`#222026`], border-[`#eee8f4`], bg-[`#fbf7ff`], hover:bg-[`#fbf7ff`],
text-[`#6252ac`], etc.) — replace them with design token classes (e.g.,
text-primary, border-border, bg-background, bg-surface, text-accent,
hover:bg-surface) and lift repeated patterns into shared UI classes/components;
update usages inside IntegrationRow, InfoCard, the main wrapper
(MoimShell/MoimTopBar section), form controls and PurpleButton/ProviderGlyph
wrappers to use the token classes, and move any common paddings/rounded/shadow
styles into a shared component or CSS utility in globals.css so styling follows
the Tailwind/designtoken convention.

In `@src/app/page.tsx`:
- Around line 123-133: The current code nests PurpleButton and Button inside
Link (Link + PurpleButton, Link + Button), causing semantic/accessibility
issues; update the Link usage to not render a nested button by using Link's
asChild prop (e.g., <Link asChild href="/signup"> and <Link asChild
href="/schedule/create">) and let PurpleButton and Button render as the anchor
child, or alternatively render Link itself with the button styling instead of
nesting—adjust PurpleButton/Button props if needed so they render correctly as
the anchor child.
- Around line 14-135: The page uses many hardcoded HEX colors; replace those
with the design tokens defined in src/app/globals.css by swapping literal colors
in className strings to semantic token classes or Tailwind utility variants that
reference the tokens (e.g., bg-[var(--token-...)] / text-[var(--token-...)]).
Update occurrences in the MoimShell wrapper, the badge, headings, paragraph
text, the calendar list card, token-colored buttons (PurpleButton, Button), the
settings card (HeatmapGrid container), and icon wrappers (CalendarDays,
CheckCircle2, Sparkles) to use the appropriate semantic tokens (primary,
background, surface, muted, accent, etc.) so all components (MoimTopBar,
SchedulerPreview, HeatmapGrid) follow global tokens instead of hardcoded HEX
values.

In `@src/app/schedule/`[id]/ScheduleRoomClient.tsx:
- Around line 689-695: The containsSlot function is duplicated (in
ScheduleRoomClient.tsx and src/lib/schedules/store.ts); extract it to a single
exported helper (e.g., export function containsSlot(container: TimeSlot, target:
TimeSlot) in a new module like src/lib/scheduling/time-slot.ts), update both
ScheduleRoomClient.tsx and src/lib/schedules/store.ts to import { containsSlot }
from that module, and remove the local implementations so both locations use the
shared implementation.

In `@src/app/schedule/create/CreateScheduleClient.tsx`:
- Around line 292-318: LinkField renders a Copy icon but doesn't implement
clipboard functionality; update LinkField to add a click handler on the Copy
element that calls navigator.clipboard.writeText(value) (handle the returned
promise and log or silently ignore errors), make the Copy element
keyboard-accessible (add role="button" and tabIndex={0} and an aria-label like
"Copy link"), and ensure the input value used is the same prop (value) so
clicking the Copy icon actually copies the displayed link; reference: LinkField,
value prop, Copy element.

In `@src/components/moim/reference-ui.tsx`:
- Around line 67-69: The span rendering a hardcoded avatar initial "지" should be
replaced to use dynamic user data: update the component (reference-ui / the
React component that renders the span) to accept a prop like user (or
avatarInitial / displayName) and render the initial from that prop (with a safe
fallback such as the first character of displayName or a default icon), or add a
TODO comment if it's intentionally a mock; specifically replace the literal "지"
in the span with a computed value derived from props (e.g., avatarInitial ||
displayName?.[0] || fallback) and ensure prop types/defaultProps (or TypeScript
types) reflect the new prop.

In `@src/lib/schedules/__tests__/store.test.ts`:
- Around line 147-223: Add two edge-case tests for confirmSchedule: (1) "rejects
confirmation when no participants exist" — createSchedule with no
addParticipantAvailability calls and assert confirmSchedule(created.id,
created.hostToken, slot) rejects with "common slots"; (2) "rejects
re-confirmation when already confirmed" — createSchedule,
addParticipantAvailability for participants to produce a valid common slot, call
confirmSchedule once successfully, then call confirmSchedule again with the same
hostToken and slot and assert it rejects (expect an error about already
confirmed or invalid state). Use the existing helpers createSchedule,
addParticipantAvailability, confirmSchedule and
getSchedulePublic/created.hostToken to locate where to insert these tests.

---

Duplicate comments:
In `@scripts/ensure-sqlite-schema.mjs`:
- Around line 7-102: Wrap all DDL operations (calls to prisma.$executeRawUnsafe,
prisma.$queryRawUnsafe and the scheduleColumns logic) in a try/finally so
prisma.$disconnect() is always called; i.e., open a try before the first
prisma.$executeRawUnsafe, perform the CREATE TABLE/INDEX/PRAGMA/ALTER operations
inside the try, and move the prisma.$disconnect() into the finally block to
ensure it runs even if any prisma.$executeRawUnsafe or prisma.$queryRawUnsafe
throws.

In `@src/lib/schedules/store.ts`:
- Around line 173-176: The clearSchedules function must perform both deletions
atomically to avoid inconsistent state if an error occurs between them; replace
the two separate calls to prisma.scheduleParticipant.deleteMany and
prisma.schedule.deleteMany with a single Prisma transaction (e.g., use
prisma.$transaction) that runs both deletes (participants first, then schedules)
within the same transaction scope so either both succeed or both roll back.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3e339f87-2509-4574-b1a3-1eeb9a7ea7a0

📥 Commits

Reviewing files that changed from the base of the PR and between e632bc6 and e8fa530.

📒 Files selected for processing (16)
  • e2e/participant-flow.spec.ts
  • next.config.ts
  • postcss.config.js
  • prisma/migrations/20260528032000_add_schedule_confirmation/migration.sql
  • prisma/schema.prisma
  • scripts/ensure-sqlite-schema.mjs
  • src/app/(auth)/login/page.tsx
  • src/app/(auth)/signup/page.tsx
  • src/app/api/schedules/[id]/route.ts
  • src/app/calendar/connect/page.tsx
  • src/app/page.tsx
  • src/app/schedule/[id]/ScheduleRoomClient.tsx
  • src/app/schedule/create/CreateScheduleClient.tsx
  • src/components/moim/reference-ui.tsx
  • src/lib/schedules/__tests__/store.test.ts
  • src/lib/schedules/store.ts

Comment thread e2e/participant-flow.spec.ts Outdated
Comment on lines +37 to +38
await page.getByRole("button", { name: "이 시간 확정" }).first().click();
await expect(page.getByText("시간이 확정되었습니다")).toBeVisible();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

first() 기반 클릭은 플래키합니다. 슬롯 문맥으로 버튼을 좁히세요.

Why: 동일 라벨 버튼이 여러 개일 때 DOM 순서 변경만으로 테스트가 깨집니다.
How: 확정하려는 슬롯 텍스트 영역 내부의 버튼을 선택하세요.

수정 예시
- await page.getByRole("button", { name: "이 시간 확정" }).first().click();
+ const commonSlots = page.getByTestId("common-slots");
+ const targetRow = commonSlots.getByText("월요일 10:00-12:00").locator("..");
+ await targetRow.getByRole("button", { name: "이 시간 확정" }).click();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/participant-flow.spec.ts` around lines 37 - 38, The test currently uses
page.getByRole("button", { name: "이 시간 확정" }).first().click(), which is flaky
when multiple same-label buttons exist; instead locate the specific slot
container (e.g., use page.getByText("<slot label/text>").locator or page.locator
for the slot element) and call locator.getByRole("button", { name: "이 시간 확정"
}).click() within that slot context so the click targets the intended slot
button; keep the subsequent expect(page.getByText("시간이 확정되었습니다")).toBeVisible()
assertion unchanged.

Comment thread src/app/(auth)/login/page.tsx Outdated
Comment on lines +92 to +94
<Link href="/login" className="text-base text-[#6252ac]">
비밀번호 찾기
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

비밀번호 찾기 링크가 자기 자신을 참조

href="/login"은 현재 페이지와 동일. 의도된 동작인지 확인 필요.

Why: 사용자가 비밀번호 찾기 클릭 시 같은 페이지 유지 → UX 혼란.

How: 올바른 경로로 수정하거나 기능 미구현 시 비활성화.

-<Link href="/login" className="text-base text-[`#6252ac`]">
+<Link href="/forgot-password" className="text-base text-[`#6252ac`]">
   비밀번호 찾기
 </Link>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Link href="/login" className="text-base text-[#6252ac]">
비밀번호 찾기
</Link>
<Link href="/forgot-password" className="text-base text-[`#6252ac`]">
비밀번호 찾기
</Link>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(auth)/login/page.tsx around lines 92 - 94, The "비밀번호 찾기" Link
currently points to the same page (Link with href="/login"); update it to the
intended password-recovery route (e.g., change href to "/forgot-password" or
"/reset-password") or, if the feature isn't implemented yet, disable the link by
replacing it with a non-interactive element or add aria-disabled and
preventDefault on click so it doesn't navigate back to the login page; ensure
you modify the Link element's href and/or element type in page.tsx accordingly.

Comment thread src/app/(auth)/signup/page.tsx Outdated
Comment on lines +40 to +43
if (form.password !== form.passwordConfirm) {
throw new Error("비밀번호 확인이 일치하지 않습니다.");
}
const response = await fetch("/api/auth/signup", {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

필수 약관 체크를 제출 전에 강제하세요.

Why: 현재는 필수로 표시만 하고 실제 검증이 없어 미동의 상태로 가입 요청이 전송될 수 있습니다(컴플라이언스/데이터 정합성 위험).
How: submit 단계에서 필수 플래그를 검증하고, 체크박스에도 required를 부여하세요.

수정 예시
  if (form.password !== form.passwordConfirm) {
    throw new Error("비밀번호 확인이 일치하지 않습니다.");
  }
+ if (!form.isAgeOver14 || !form.termsAgreed || !form.privacyAgreed) {
+   throw new Error("필수 약관에 모두 동의해 주세요.");
+ }

  <input
    type="checkbox"
    checked={Boolean(form[name as keyof typeof form])}
+   required={name === "isAgeOver14" || name === "termsAgreed" || name === "privacyAgreed"}
    onChange={(event) =>

Also applies to: 145-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(auth)/signup/page.tsx around lines 40 - 43, Before sending the
signup request, enforce that all required agreement checkboxes are checked and
mark the checkbox inputs as required: in the signup page's submit handler (the
function that currently checks form.password and form.passwordConfirm and then
calls fetch("/api/auth/signup")), add validation that verifies the required
agreement flags on the form state (e.g., termsAccepted, privacyAccepted) are
true and throw a user-facing error if not; also update the checkbox inputs for
the mandatory agreements (the JSX inputs rendered around the areas referenced
145-166) to include the HTML required attribute so browsers enforce them too.
Ensure the validation occurs prior to the password-confirm check and the fetch
call so no request is sent when required agreements are unchecked.

Comment thread src/app/(auth)/signup/page.tsx Outdated
Comment on lines +63 to +299
<main className="min-h-screen bg-white px-6 py-14 text-[#222026]">
<section className="mx-auto w-full max-w-[420px]">
<div className="mb-10 text-center">
<Link
href="/"
className="text-6xl font-extrabold tracking-normal text-[#6252ac]"
>
MOIM
</Link>
<p className="mt-4 text-lg font-semibold text-[#6f6a73]">
모임을 더 가깝게, 일상을 더 특별하게
</p>
</div>

{isComplete ? (
<div className="rounded-[2rem] border border-[#eee8f4] bg-white p-10 text-center shadow-[0_20px_60px_rgba(95,82,130,0.12)]">
<CheckCircle2 className="mx-auto h-14 w-14 text-[#6252ac]" />
<h1 className="mt-5 text-3xl font-extrabold">회원가입 완료</h1>
<p className="mt-3 text-[#77727c]">
이제 캘린더를 연동하거나 바로 모임을 만들 수 있습니다.
</p>
<div className="mt-8 grid gap-3">
<Link
href="/calendar/connect"
className="inline-flex h-12 items-center justify-center rounded-xl bg-[#8f7bd6] font-bold text-white"
>
캘린더 연동하기
</Link>
<Link
href="/schedule/create"
className="inline-flex h-12 items-center justify-center rounded-xl border border-[#eee8f4] font-bold text-[#6252ac]"
>
모임 만들기
</Link>
</div>
</div>
) : (
<>
<div className="grid gap-4">
<SocialButton type="kakao" label="카카오로 시작하기" />
<SocialButton type="google" label="구글로 시작하기" />
<SocialButton type="naver" label="네이버로 시작하기" />
<SocialButton type="apple" label="애플로 시작하기" dark />
</div>

<div className="my-8 h-px bg-[#dedbe3]" />

<form onSubmit={handleSubmit} className="grid gap-6">
<TextField
label="이메일"
value={form.email}
onChange={(value) => updateField("email", value)}
type="email"
autoComplete="email"
/>
<TextField
label="전화번호"
value={form.phoneNumber}
onChange={(value) => updateField("phoneNumber", value)}
autoComplete="tel"
/>
<TextField
label="닉네임"
value={form.nickname}
onChange={(value) => updateField("nickname", value)}
autoComplete="nickname"
/>
<PasswordField
label="비밀번호"
value={form.password}
onChange={(value) => updateField("password", value)}
/>
<PasswordField
label="비밀번호 확인"
value={form.passwordConfirm}
onChange={(value) => updateField("passwordConfirm", value)}
/>

<fieldset className="rounded-lg border border-[#dedbe3] p-5">
<legend className="px-1 text-lg font-bold">약관 동의</legend>
<div className="grid gap-4">
{[
["isAgeOver14", "만 14세 이상입니다", "필수"],
["termsAgreed", "이용약관", "필수"],
["privacyAgreed", "개인정보수집 및 이용동의", "필수"],
["marketingAgreed", "개인정보 마케팅 활용 동의", "선택"],
["eventSmsAgreed", "이벤트, 쿠폰 및 SMS 등 수신", "선택"],
].map(([name, label, required]) => (
<label
key={name}
className="flex items-center justify-between gap-3 text-base font-semibold text-[#504b55]"
>
<span className="flex items-center gap-3">
<input
type="checkbox"
checked={Boolean(form[name as keyof typeof form])}
onChange={(event) =>
updateField(
name as keyof typeof form,
event.target.checked,
)
}
className="h-5 w-5 rounded border-[#dedbe3]"
/>
{label}
<span className="text-[#8f7bd6]">({required})</span>
</span>
<ChevronRight className="h-5 w-5 text-[#aaa5ad]" />
</label>
))}
</div>
</fieldset>

{message ? (
<p role="alert" className="text-sm text-destructive">
{message}
</p>
) : null}

<PurpleButton
type="submit"
className="w-full"
disabled={isSubmitting}
>
{isSubmitting ? "가입 중" : "회원가입"}
</PurpleButton>
</form>
<p className="mt-6 text-center text-lg font-semibold text-[#6f6a73]">
계정이 있으신가요?{" "}
<Link href="/login" className="text-[#6252ac]">
로그인
</Link>
</p>
</>
)}
</section>
</main>
);
}

function SocialButton({
type,
label,
dark = false,
}: {
type: "kakao" | "google" | "naver" | "apple";
label: string;
dark?: boolean;
}) {
const href = type === "kakao" ? "/api/auth/kakao/login" : undefined;
const className = dark
? "bg-[#171717] text-white"
: type === "kakao"
? "bg-[#fee500] text-[#191919]"
: type === "naver"
? "bg-[#03c75a] text-white"
: "border border-[#dedbe3] bg-white text-[#222026]";
const content = (
<>
<ProviderGlyph type={type} />
{label}
</>
);
if (href) {
return (
<a
href={href}
className={`inline-flex h-14 items-center justify-center gap-3 rounded-lg text-lg font-bold ${className}`}
>
<MessageCircle className="h-5 w-5" />
{label}
</a>
);
}
return (
<button
type="button"
className={`inline-flex h-14 items-center justify-center gap-3 rounded-lg text-lg font-bold ${className}`}
>
{content}
</button>
);
}

function TextField({
label,
value,
onChange,
type = "text",
autoComplete,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
autoComplete?: string;
}) {
return (
<label className="grid gap-2 text-lg font-bold">
{label}
<input
type={type}
value={value}
onChange={(event) => onChange(event.target.value)}
className="h-14 rounded-lg border border-[#dedbe3] px-4 text-lg font-normal outline-none focus:border-[#8f7bd6] focus:ring-2 focus:ring-[#ece7fb]"
autoComplete={autoComplete}
required
/>
</label>
);
}

function PasswordField({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
return (
<label className="grid gap-2 text-lg font-bold">
{label}
<span className="relative">
<input
type="password"
value={value}
onChange={(event) => onChange(event.target.value)}
className="h-14 w-full rounded-lg border border-[#dedbe3] px-4 pr-12 text-lg font-normal outline-none focus:border-[#8f7bd6] focus:ring-2 focus:ring-[#ece7fb]"
autoComplete="new-password"
required
/>
<EyeOff className="absolute right-4 top-1/2 h-5 w-5 -translate-y-1/2 text-[#aaa5ad]" />
</span>
</label>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

회원가입 페이지 색상도 토큰 기반으로 통일하세요.

Why: 페이지 단위로 HEX 하드코딩이 누적되면 테마 변경/디자인 시스템 유지가 어려워집니다.
How: globals.css 토큰(class)으로 치환하고 공통 버튼/입력 스타일은 shadcn/ui 계층으로 흡수하세요.

수정 예시
- <main className="min-h-screen bg-white px-6 py-14 text-[`#222026`]">
+ <main className="min-h-screen bg-background px-6 py-14 text-foreground">

- className="h-14 rounded-lg border border-[`#dedbe3`] ..."
+ className="h-14 rounded-lg border border-border ..."

- className="text-[`#6252ac`]"
+ className="text-primary"

As per coding guidelines **/*.{css,ts,tsx}: Use Tailwind CSS v3 with shadcn/ui components for styling, with design tokens and global styles defined in src/app/globals.css.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(auth)/signup/page.tsx around lines 63 - 299, The page uses
hardcoded HEX colors throughout; replace these with Tailwind token classes from
globals.css and migrate common UI bits to shadcn/ui components: swap direct hex
classes (e.g., "text-[`#6252ac`]", "bg-[`#8f7bd6`]", "border-[`#dedbe3`]",
"text-[`#6f6a73`]", etc.) in the render of the default export and in components
SocialButton, TextField, PasswordField, and usage of PurpleButton/CheckCircle2
to the corresponding design-token classes (e.g., text-primary, bg-primary-500,
border-muted, text-secondary) defined in src/app/globals.css; extract shared
button/input styles into shadcn/ui Button/Input variants and use those
components instead of raw <button>/<input> where possible, ensuring
autoComplete/props remain unchanged and preserving className merges for dark
variants in SocialButton.

Comment thread src/app/api/schedules/[id]/route.ts Outdated
Comment on lines +37 to +60
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const body = await request.json();
const schedule = await confirmSchedule(
id,
body.hostToken,
body.confirmedSlot,
);
return NextResponse.json({ schedule });
} catch (error) {
const message = error instanceof Error ? error.message : "invalid request";
const status =
message === "schedule not found"
? 404
: message === "invalid host token"
? 403
: 400;
return NextResponse.json({ error: message }, { status });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

PATCH body 타입 검증 부재

body.hostToken, body.confirmedSlot이 undefined이거나 잘못된 타입일 경우 store 레이어에서 예외 발생. API 레이어에서 조기 검증 권장.

Why: 명확한 400 에러 메시지 제공, store 레이어 불필요 호출 방지.

How: 간단한 타입 가드 추가.

♻️ 입력 검증 추가
 export async function PATCH(
   request: NextRequest,
   { params }: { params: Promise<{ id: string }> },
 ) {
   try {
     const { id } = await params;
     const body = await request.json();
+
+    if (typeof body.hostToken !== "string" || !body.confirmedSlot) {
+      return NextResponse.json(
+        { error: "hostToken and confirmedSlot are required" },
+        { status: 400 },
+      );
+    }
+
     const schedule = await confirmSchedule(
       id,
       body.hostToken,
       body.confirmedSlot,
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const body = await request.json();
const schedule = await confirmSchedule(
id,
body.hostToken,
body.confirmedSlot,
);
return NextResponse.json({ schedule });
} catch (error) {
const message = error instanceof Error ? error.message : "invalid request";
const status =
message === "schedule not found"
? 404
: message === "invalid host token"
? 403
: 400;
return NextResponse.json({ error: message }, { status });
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const body = await request.json();
if (typeof body.hostToken !== "string" || !body.confirmedSlot) {
return NextResponse.json(
{ error: "hostToken and confirmedSlot are required" },
{ status: 400 },
);
}
const schedule = await confirmSchedule(
id,
body.hostToken,
body.confirmedSlot,
);
return NextResponse.json({ schedule });
} catch (error) {
const message = error instanceof Error ? error.message : "invalid request";
const status =
message === "schedule not found"
? 404
: message === "invalid host token"
? 403
: 400;
return NextResponse.json({ error: message }, { status });
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/schedules/`[id]/route.ts around lines 37 - 60, The PATCH handler
calls confirmSchedule(id, body.hostToken, body.confirmedSlot) without validating
request body, which can throw from the store layer; add a simple type guard
after reading body in PATCH to check that body.hostToken is a non-empty string
and body.confirmedSlot has the expected shape/type (e.g., non-null object or
specific primitive) and if validation fails return NextResponse.json({ error:
"invalid request body" }, { status: 400 }) instead of calling confirmSchedule;
update references in this function (PATCH, params, body.hostToken,
body.confirmedSlot, confirmSchedule) only—no store calls should run when
validation fails.

Comment thread src/app/page.tsx Outdated
Comment on lines +123 to +133
<Link href="/signup">
<PurpleButton className="w-44">가입하기</PurpleButton>
</Link>
<Link href="/schedule/create">
<Button
variant="outline"
className="h-12 w-44 rounded-xl border-[#eee8f4] bg-white text-[#8f7bd6]"
>
문의하기
</Button>
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Link 안에 버튼 컴포넌트를 중첩하지 마세요.

Why: Link가 렌더링하는 앵커와 버튼 계열 컴포넌트가 중첩되면 시맨틱/접근성 문제가 발생하고 포커스 동작이 불안정해집니다.
How: 버튼을 asChild로 앵커를 직접 감싸거나, Link를 버튼 스타일로 직접 렌더링하세요.

수정 예시
- <Link href="/signup">
-   <PurpleButton className="w-44">가입하기</PurpleButton>
- </Link>
+ <PurpleButton asChild className="w-44">
+   <Link href="/signup">가입하기</Link>
+ </PurpleButton>

- <Link href="/schedule/create">
-   <Button
-     variant="outline"
-     className="h-12 w-44 rounded-xl border-[`#eee8f4`] bg-white text-[`#8f7bd6`]"
-   >
-     문의하기
-   </Button>
- </Link>
+ <Button
+   asChild
+   variant="outline"
+   className="h-12 w-44 rounded-xl border-[`#eee8f4`] bg-white text-[`#8f7bd6`]"
+ >
+   <Link href="/schedule/create">문의하기</Link>
+ </Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/page.tsx` around lines 123 - 133, The current code nests PurpleButton
and Button inside Link (Link + PurpleButton, Link + Button), causing
semantic/accessibility issues; update the Link usage to not render a nested
button by using Link's asChild prop (e.g., <Link asChild href="/signup"> and
<Link asChild href="/schedule/create">) and let PurpleButton and Button render
as the anchor child, or alternatively render Link itself with the button styling
instead of nesting—adjust PurpleButton/Button props if needed so they render
correctly as the anchor child.

Comment on lines +689 to +695
function containsSlot(container: TimeSlot, target: TimeSlot): boolean {
return (
container.day === target.day &&
container.startHour <= target.startHour &&
container.endHour >= target.endHour
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

containsSlot 함수 중복

src/lib/schedules/store.ts에 동일 로직 존재. DRY 위반.

Why: 로직 변경 시 두 곳 모두 수정 필요 → 불일치 위험.

How: 공통 모듈에서 export하여 재사용.

// src/lib/scheduling/time-slot.ts 또는 유사 위치에 추가
export function containsSlot(container: TimeSlot, target: TimeSlot): boolean {
  return (
    container.day === target.day &&
    container.startHour <= target.startHour &&
    container.endHour >= target.endHour
  );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/`[id]/ScheduleRoomClient.tsx around lines 689 - 695, The
containsSlot function is duplicated (in ScheduleRoomClient.tsx and
src/lib/schedules/store.ts); extract it to a single exported helper (e.g.,
export function containsSlot(container: TimeSlot, target: TimeSlot) in a new
module like src/lib/scheduling/time-slot.ts), update both ScheduleRoomClient.tsx
and src/lib/schedules/store.ts to import { containsSlot } from that module, and
remove the local implementations so both locations use the shared
implementation.

Comment on lines 292 to 318
function LinkField({
label,
testId,
value,
}: {
label: string;
testId: string;
value: string;
}) {
return (
<label className="grid gap-2 text-sm font-bold text-[#4f4a55]">
<span className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-[#6252ac]" />
{label}
</span>
<span className="relative">
<input
data-testid={testId}
value={value}
readOnly
className="h-12 w-full rounded-xl border border-[#dedbe3] bg-[#fbf7ff] px-4 pr-12 text-sm outline-none"
/>
<Copy className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-[#aaa5ad]" />
</span>
</label>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

LinkField 복사 기능 미구현

Copy 아이콘이 렌더링되지만 실제 클립보드 복사 기능이 없음. UX 기대와 불일치.

Why: 사용자가 아이콘을 클릭해도 아무 동작 없음 → 혼란 유발.

How: navigator.clipboard.writeText 호출 추가.

♻️ 복사 기능 추가
 function LinkField({
   label,
   testId,
   value,
 }: {
   label: string;
   testId: string;
   value: string;
 }) {
+  async function handleCopy() {
+    await navigator.clipboard.writeText(value);
+  }
+
   return (
     <label className="grid gap-2 text-sm font-bold text-[`#4f4a55`]">
       <span className="flex items-center gap-2">
         <Link2 className="h-4 w-4 text-[`#6252ac`]" />
         {label}
       </span>
       <span className="relative">
         <input
           data-testid={testId}
           value={value}
           readOnly
           className="h-12 w-full rounded-xl border border-[`#dedbe3`] bg-[`#fbf7ff`] px-4 pr-12 text-sm outline-none"
         />
-        <Copy className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-[`#aaa5ad`]" />
+        <button
+          type="button"
+          onClick={handleCopy}
+          className="absolute right-4 top-1/2 -translate-y-1/2"
+          aria-label="링크 복사"
+        >
+          <Copy className="h-4 w-4 text-[`#aaa5ad`] hover:text-[`#6252ac`]" />
+        </button>
       </span>
     </label>
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function LinkField({
label,
testId,
value,
}: {
label: string;
testId: string;
value: string;
}) {
return (
<label className="grid gap-2 text-sm font-bold text-[#4f4a55]">
<span className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-[#6252ac]" />
{label}
</span>
<span className="relative">
<input
data-testid={testId}
value={value}
readOnly
className="h-12 w-full rounded-xl border border-[#dedbe3] bg-[#fbf7ff] px-4 pr-12 text-sm outline-none"
/>
<Copy className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-[#aaa5ad]" />
</span>
</label>
);
}
function LinkField({
label,
testId,
value,
}: {
label: string;
testId: string;
value: string;
}) {
async function handleCopy() {
await navigator.clipboard.writeText(value);
}
return (
<label className="grid gap-2 text-sm font-bold text-[`#4f4a55`]">
<span className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-[`#6252ac`]" />
{label}
</span>
<span className="relative">
<input
data-testid={testId}
value={value}
readOnly
className="h-12 w-full rounded-xl border border-[`#dedbe3`] bg-[`#fbf7ff`] px-4 pr-12 text-sm outline-none"
/>
<button
type="button"
onClick={handleCopy}
className="absolute right-4 top-1/2 -translate-y-1/2"
aria-label="링크 복사"
>
<Copy className="h-4 w-4 text-[`#aaa5ad`] hover:text-[`#6252ac`]" />
</button>
</span>
</label>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/create/CreateScheduleClient.tsx` around lines 292 - 318,
LinkField renders a Copy icon but doesn't implement clipboard functionality;
update LinkField to add a click handler on the Copy element that calls
navigator.clipboard.writeText(value) (handle the returned promise and log or
silently ignore errors), make the Copy element keyboard-accessible (add
role="button" and tabIndex={0} and an aria-label like "Copy link"), and ensure
the input value used is the same prop (value) so clicking the Copy icon actually
copies the displayed link; reference: LinkField, value prop, Copy element.

Comment thread src/components/moim/reference-ui.tsx Outdated
Comment on lines +67 to +69
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[#f4f0fb] text-lg font-semibold text-[#6252ac]">
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

하드코딩된 사용자 아바타 "지"

네비게이션 바에 고정 문자열 "지" 표시. 실제 사용자 데이터 연동 필요 시 문제.

Why: 데모/목업 용도라면 괜찮으나, 프로덕션 사용 시 동적 데이터 필요.

How: props로 사용자 정보 받거나 TODO 주석 추가.

-<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[`#f4f0fb`] text-lg font-semibold text-[`#6252ac`]">
-
-</span>
+{/* TODO: 로그인 사용자 정보 연동 필요 */}
+<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[`#f4f0fb`] text-lg font-semibold text-[`#6252ac`]">
+
+</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[#f4f0fb] text-lg font-semibold text-[#6252ac]">
</span>
{/* TODO: 로그인 사용자 정보 연동 필요 */}
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[`#f4f0fb`] text-lg font-semibold text-[`#6252ac`]">
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/moim/reference-ui.tsx` around lines 67 - 69, The span
rendering a hardcoded avatar initial "지" should be replaced to use dynamic user
data: update the component (reference-ui / the React component that renders the
span) to accept a prop like user (or avatarInitial / displayName) and render the
initial from that prop (with a safe fallback such as the first character of
displayName or a default icon), or add a TODO comment if it's intentionally a
mock; specifically replace the literal "지" in the span with a computed value
derived from props (e.g., avatarInitial || displayName?.[0] || fallback) and
ensure prop types/defaultProps (or TypeScript types) reflect the new prop.

Comment on lines +147 to +223
test("lets the host confirm one of the common slots", async () => {
const created = await createSchedule({
title: "주간 회의",
durationMinutes: 60,
candidateDays: ["FRI"],
candidateStartHour: 18,
candidateEndHour: 22,
});

await addParticipantAvailability(created.id, {
name: "민지",
available: [{ day: "FRI", startHour: 19, endHour: 21 }],
});
await addParticipantAvailability(created.id, {
name: "준호",
available: [{ day: "FRI", startHour: 19, endHour: 22 }],
});

const confirmed = await confirmSchedule(created.id, created.hostToken, {
day: "FRI",
startHour: 19,
endHour: 20,
});

expect(confirmed.status).toBe("confirmed");
expect(confirmed.confirmedSlot).toEqual({
day: "FRI",
startHour: 19,
endHour: 20,
});
expect((await getSchedulePublic(created.id))?.status).toBe("confirmed");
});

test("rejects confirmation with an invalid host token", async () => {
const created = await createSchedule({
title: "주간 회의",
durationMinutes: 60,
candidateDays: ["FRI"],
candidateStartHour: 18,
candidateEndHour: 22,
});

await expect(
confirmSchedule(created.id, "wrong-token", {
day: "FRI",
startHour: 19,
endHour: 20,
}),
).rejects.toThrow("invalid host token");
});

test("rejects confirmation outside the current common slots", async () => {
const created = await createSchedule({
title: "주간 회의",
durationMinutes: 60,
candidateDays: ["FRI"],
candidateStartHour: 18,
candidateEndHour: 22,
});

await addParticipantAvailability(created.id, {
name: "민지",
available: [{ day: "FRI", startHour: 19, endHour: 20 }],
});
await addParticipantAvailability(created.id, {
name: "준호",
available: [{ day: "FRI", startHour: 20, endHour: 21 }],
});

await expect(
confirmSchedule(created.id, created.hostToken, {
day: "FRI",
startHour: 19,
endHour: 20,
}),
).rejects.toThrow("common slots");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

엣지 케이스 테스트 누락

confirmSchedule 테스트에서 참여자 없는 상태, 이미 확정된 상태 재확정 케이스 미검증.

Why: 경계 조건 커버리지 부족 → 프로덕션 버그 가능성.

How: 추가 테스트 케이스.

test("rejects confirmation when no participants exist", async () => {
  const created = await createSchedule({
    title: "빈 모임",
    durationMinutes: 60,
    candidateDays: ["FRI"],
    candidateStartHour: 18,
    candidateEndHour: 22,
  });

  await expect(
    confirmSchedule(created.id, created.hostToken, {
      day: "FRI",
      startHour: 19,
      endHour: 20,
    }),
  ).rejects.toThrow("common slots");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schedules/__tests__/store.test.ts` around lines 147 - 223, Add two
edge-case tests for confirmSchedule: (1) "rejects confirmation when no
participants exist" — createSchedule with no addParticipantAvailability calls
and assert confirmSchedule(created.id, created.hostToken, slot) rejects with
"common slots"; (2) "rejects re-confirmation when already confirmed" —
createSchedule, addParticipantAvailability for participants to produce a valid
common slot, call confirmSchedule once successfully, then call confirmSchedule
again with the same hostToken and slot and assert it rejects (expect an error
about already confirmed or invalid state). Use the existing helpers
createSchedule, addParticipantAvailability, confirmSchedule and
getSchedulePublic/created.hostToken to locate where to insert these tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant