Skip to content

[Feat/#81] 클라이언트 Auth Apis 연동#82

Merged
skyblue1232 merged 9 commits intodevelopfrom
feat/#81/signup-api
Apr 2, 2026
Merged

[Feat/#81] 클라이언트 Auth Apis 연동#82
skyblue1232 merged 9 commits intodevelopfrom
feat/#81/signup-api

Conversation

@skyblue1232
Copy link
Copy Markdown
Contributor

@skyblue1232 skyblue1232 commented Apr 2, 2026

✅ 작업 내용

📝 Description

  • 로그인 / 회원가입 API 연동 및 React Query 기반 mutation 추가
  • 로그인/회원가입 UI 컴포넌트 분리 (Form, Button 등)
  • owner 회원가입 → 사업자 인증 → 매장 등록 흐름 구현
  • 매장 정보 수정 페이지 API 연동 (store, location, image, randomBox)
  • register 관련 타입/상수 제거 → API 모델 기반으로 리팩토링
  • BusinessHours / RandomBox 모달 구조 개선 및 서버 데이터 구조에 맞게 수정

작업한 내용을 체크해주세요.

  • 로그인 / 회원가입 API 연동
  • React Query mutation 구조 적용
  • 로그인/회원가입 UI 컴포넌트 분리
  • owner 회원가입 flow 구현
  • 매장 정보 수정 API 연동
  • register 구조 → API 모델 기반 리팩토링
  • 모달 (영업시간, 랜덤박스) 로직 개선

🚀 설계 의도 및 개선점

  • API 패키지(@compasser/api) 기반으로 데이터 흐름 일원화
  • form 상태를 컴포넌트 외부에서 관리하도록 분리하여 재사용성 확보
  • 기존 임의 데이터/상수 제거 → 서버 응답 기반 구조로 전환
  • mutation 단위로 기능 분리하여 유지보수성과 확장성 개선
  • owner register / store edit를 동일한 구조로 맞춰 일관성 확보

📎 기타 참고사항

  • accessToken localStorage 저장 방식 사용
  • API baseURL: env 기반 설정

Fixes #81

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 고객 앱 로그인 페이지에 카카오 로그인 기능 추가
    • 회원가입 프로세스 개선 및 비즈니스 인증 단계 추가
    • 가게 정보 관리 기능 강화 (사진 업로드, 무작위 박스 관리)
  • 개선사항

    • 로그인 및 회원가입 폼의 유효성 검사 강화
    • 비즈니스 번호 검증 추가
    • API 통합 및 상태 관리 개선

@skyblue1232 skyblue1232 self-assigned this Apr 2, 2026
@skyblue1232 skyblue1232 added fix 버그 및 에러 수정 feat 기능 구현 및 생성 refactor This doesn't seem right api 서버 - 클라이언트 통신 chore 자잘한 수정 and removed fix 버그 및 에러 수정 labels Apr 2, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compasser-customer Ready Ready Preview, Comment Apr 2, 2026 6:42pm
compasser-owner Ready Ready Preview, Comment Apr 2, 2026 6:42pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

개요

고객 및 사업자 앱에서 React Query 기반 인증 플로우를 구현하고, 로그인/가입 페이지를 컴포넌트로 분해하며, API 토큰 관리 시스템을 추가하고, 등록 폼 상태를 제어 컴포넌트로 리팩터링했습니다. Zustand 스토어를 통한 상태 관리 및 여러 뮤테이션/쿼리 훅을 도입했습니다.

변경 사항

Cohort / File(s) 요약
고객 로그인 컴포넌트
apps/customer/src/app/login/_components/KakaoLoginButton.tsx, LoginForm.tsx, SignupLinkButton.tsx
새로운 버튼 및 폼 컴포넌트를 추가하여 로그인 UI를 모듈화하고 props 기반으로 동작을 제어합니다.
고객 로그인 상수
apps/customer/src/app/login/_constants/login.constants.ts
로그인 UI의 한국어 텍스트 문자열을 정의하는 상수를 추가합니다.
고객 로그인 페이지
apps/customer/src/app/login/page.tsx
로그인 폼을 컴포넌트로 분해하고 useLoginMutation 훅으로 실제 뮤테이션 플로우를 구현하며 토큰 저장소에 접근 토큰을 저장합니다.
고객 가입 페이지
apps/customer/src/app/signup/page.tsx
useSignupMutation 훅을 추가하여 폼 유효성 검사 및 비동기 가입 처리를 수행합니다.
고객 API 모듈
apps/customer/src/shared/api/api.ts
localStorage 기반 토큰 스토어와 함께 API 클라이언트를 초기화하고 인증 모듈을 노출합니다.
고객 인증 뮤테이션
apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts, useSignupMutation.ts
로그인 및 가입 뮤테이션을 위한 React Query 훅을 제공합니다.
사업자 API 모듈
apps/owner/src/shared/api/api.ts
ownerAccessToken/ownerRefreshToken 기반 토큰 스토어로 API 클라이언트를 설정하고 여러 모듈(auth, owner, store, randomBox, storeImage)을 노출합니다.
사업자 인증/저장소 뮤테이션
apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts, useVerifyBusinessMutation.ts, usePatchMyStoreMutation.ts, usePatchMyStoreLocationMutation.ts, useCreateRandomBoxMutation.ts, useUpdateRandomBoxMutation.ts, useDeleteRandomBoxMutation.ts, useUploadStoreImageMutation.ts, useRemoveStoreImageMutation.ts
가입, 사업자 검증, 매장 정보/위치/이미지 관리, 랜덤박스 CRUD 뮤테이션을 위한 React Query 훅을 추가합니다.
사업자 저장소 쿼리
apps/owner/src/shared/queries/query/useMyStoreQuery.ts, useRandomBoxListQuery.ts, useStoreImageQuery.ts
매장 정보, 랜덤박스 목록, 매장 이미지 조회를 위한 React Query 훅을 제공합니다.
사업자 가입 페이지
apps/owner/src/app/signup/page.tsx
폼 필드를 제어 컴포넌트로 전환하고 useSignupMutation을 통한 유효성 검사 및 비동기 처리를 구현합니다.
사업자 사업자 검증 페이지
apps/owner/src/app/signup/business/page.tsx
사업자등록번호 입력/검증 기능을 추가하고 useVerifyBusinessMutation 훅으로 검증 뮤테이션을 처리합니다. 라우트 가드를 통한 단계별 진행을 구현합니다.
사업자 등록 페이지 및 컴포넌트
apps/owner/src/app/signup/register/page.tsx, _components/fields/EmailField.tsx, StoreNameField.tsx, StoreAddressField.tsx, AccountField.tsx, sections/TagSection.tsx, RandomBoxSection.tsx, PhotoUploadSection.tsx, modals/BusinessHoursModal.tsx, RandomBoxModal.tsx
등록 폼을 제어 컴포넌트로 리팩터링하고 쿼리 훅으로 초기 데이터를 로드하며, 태그는 다중선택에서 단일선택으로, 랜덤박스는 로컬 배열에서 뮤테이션 기반으로 변경합니다. 비즈니스 시간 및 랜덤박스 모달의 데이터 구조를 정규화합니다.
사업자 등록 타입/상수 제거
apps/owner/src/app/signup/register/_types/register.ts, _constants/register.ts
모듈별 타입 및 상수 파일을 제거하고 관련 정의를 새로운 유틸리티 모듈로 이관합니다.
사업자 등록 유틸리티
apps/owner/src/app/signup/register/_utils/business-hours.ts
비즈니스 시간 데이터 타입(DayKey, BusinessHoursValue) 및 파싱 함수를 정의합니다.
사업자 매장 정보 수정 페이지
apps/owner/src/app/(tabs)/mypage/store-info/page.tsx
로컬 상수 기반 상태에서 쿼리 훅 기반 데이터 페칭으로 전환하고, CRUD 작업을 로컬 배열 조작에서 비동기 뮤테이션으로 변경합니다.
사업자 가입 상태 관리
apps/owner/src/shared/stores/ownerSignup.store.ts
Zustand를 사용하여 가입 단계(signupbusinessregisterdone) 및 완료 플래그를 관리하고 로컬 스토리지에 지속화합니다.
공유 유틸리티
apps/owner/src/shared/utils/bank.ts, businessLicense.ts
은행 이름 필터링 및 계좌 번호 정규화, 사업자등록번호 검증 함수를 제공합니다.
패키지 의존성
package.json
상태 관리를 위해 zustand 5.0.12를 추가합니다.

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant LoginPage as 로그인 페이지
    participant LoginForm as LoginForm<br/>컴포넌트
    participant useLoginMutation as useLoginMutation<br/>훅
    participant API as API 클라이언트
    participant Server as 서버
    participant Storage as localStorage

    User->>LoginPage: 이메일/비밀번호 입력
    LoginPage->>LoginForm: 값 변경 감지
    LoginForm->>LoginForm: 상태 업데이트
    User->>LoginForm: 로그인 버튼 클릭
    LoginForm->>useLoginMutation: handleLogin 호출
    useLoginMutation->>API: loginMutation.mutate()
    API->>Server: POST /login (email, password)
    Server-->>API: { accessToken, ... }
    API-->>useLoginMutation: 성공 응답
    useLoginMutation->>Storage: accessToken 저장
    useLoginMutation->>LoginPage: /main으로 이동
Loading
sequenceDiagram
    actor User
    participant SignupPage as 가입 페이지
    participant SignupForm as 폼 컴포넌트
    participant useSignupMutation as useSignupMutation<br/>훅
    participant API as API 클라이언트
    participant Server as 서버

    User->>SignupPage: 회원 정보 입력
    SignupPage->>SignupForm: value/onChange props 전달
    SignupForm->>SignupForm: 폼 상태 업데이트
    User->>SignupPage: 가입 버튼 클릭
    SignupPage->>SignupPage: 유효성 검사
    alt 유효성 검사 통과
        SignupPage->>useSignupMutation: mutate({ memberName, ... })
        useSignupMutation->>API: signup 호출
        API->>Server: POST /signup
        Server-->>API: 성공/실패 응답
        API-->>useSignupMutation: 결과 반환
        useSignupMutation->>SignupPage: 다음 단계로 이동 또는 오류 표시
    else 유효성 검사 실패
        SignupPage->>SignupPage: 오류 메시지 표시
    end
Loading

예상 코드 리뷰 소요 시간

🎯 4 (복잡) | ⏱️ ~60분

관련 PR

  • CUK-Compasser/FE#42: 고객 로그인 페이지의 초기 UI를 구현했으며, 이 PR은 그 페이지를 컴포넌트로 분해하고 실제 로그인/가입 뮤테이션을 연결합니다.
  • CUK-Compasser/FE#45: 고객 가입 페이지를 추가했으며, 이 PR은 그 페이지를 유효성 검사 및 뮤테이션 기반 플로우로 리팩터링합니다.
  • CUK-Compasser/FE#67: 사업자 등록 페이지 컴포넌트/타입/상수를 도입했으며, 이 PR은 그것들을 새로운 구현 및 유틸리티로 리팩터링합니다.

제안 라벨

chore

🐰 버튼과 폼이 춤을 춘다
API의 손을 잡고 서버로
토큰은 localStorage 요람에서 자고
뮤테이션의 마법으로
로그인과 가입이 완성되었네! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[Feat/#81] 클라이언트 Auth Apis 연동'은 전체 변경 사항의 핵심을 명확하게 요약합니다. 로그인 및 회원가입 API 연동이라는 주요 목표를 간결하게 표현하고 있으며, 변경 사항들이 모두 이 목표를 지원합니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#81/signup-api

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (1)
apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx (1)

46-68: ⚠️ Potential issue | 🟠 Major

중첩 <button> 구조를 제거해주세요.

현재 삭제 버튼이 업로드 버튼 내부에 있어 HTML 사양 위반입니다. 이는 접근성 문제를 야기하고 키보드/포커스 관리가 불안정해질 수 있습니다. event.stopPropagation() 처리로 일부 동작하지만 구조적 문제는 해결되지 않습니다.

🔧 제안 수정안
-      <button
-        type="button"
-        onClick={handleOpenFilePicker}
-        className="relative flex h-[18rem] w-full items-center justify-center overflow-hidden rounded-[10px] bg-background"
-      >
-        {previewUrl ? (
-          <>
+      <div className="relative">
+        <button
+          type="button"
+          onClick={handleOpenFilePicker}
+          className="flex h-[18rem] w-full items-center justify-center overflow-hidden rounded-[10px] bg-background"
+        >
+          {previewUrl ? (
             <Image
               src={previewUrl}
               alt="업로드한 대표 이미지 미리보기"
               fill
               className="object-cover"
             />
-
-            <button
-              type="button"
-              onClick={handleClickRemove}
-              className="absolute right-[0.8rem] top-[0.8rem] z-10 flex h-[3.2rem] w-[3.2rem] items-center justify-center rounded-full bg-white/90 shadow"
-              aria-label="사진 삭제"
-            >
-              <Icon name="CloseButton" width={16} height={16} />
-            </button>
-          </>
-        ) : (
-          <Icon name="Camera" width={24} height={24} />
-        )}
-      </button>
+          ) : (
+            <Icon name="Camera" width={24} height={24} />
+          )}
+        </button>
+        {previewUrl && (
+          <button
+            type="button"
+            onClick={handleClickRemove}
+            className="absolute right-[0.8rem] top-[0.8rem] z-10 flex h-[3.2rem] w-[3.2rem] items-center justify-center rounded-full bg-white/90 shadow"
+            aria-label="사진 삭제"
+          >
+            <Icon name="CloseButton" width={16} height={16} />
+          </button>
+        )}
+      </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx`
around lines 46 - 68, The delete <button> is nested inside the upload <button>
(the element using handleOpenFilePicker/previewUrl/Image), which violates HTML
specs; refactor so the delete control is not a child of the upload button: move
the delete control (currently rendering Icon and using handleClickRemove) out to
be a sibling element (e.g., a positioned <button> or <div role="button">)
adjacent to the upload <button>, preserve its styling and aria-label="사진 삭제",
and keep any event handlers (handleClickRemove) but remove reliance on
event.stopPropagation() to prevent nested-button issues; ensure keyboard focus
and onKey handlers remain functional for the new control.
🧹 Nitpick comments (4)
apps/customer/src/app/signup/page.tsx (1)

38-39: 폼 submit 이벤트로 전환하면 접근성과 입력 UX가 개선됩니다

현재는 버튼 클릭 기반이라 입력 중 Enter 제출 흐름이 약합니다. <form onSubmit> + type="submit"으로 바꾸면 키보드/모바일 키패드 UX가 좋아집니다.

Also applies to: 131-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/signup/page.tsx` around lines 38 - 39, Convert the
click-only signup flow to a proper form submission: wrap the inputs and button
in a <form> with onSubmit tied to handleSignup (replace button onClick), change
the button to type="submit", and update handleSignup to accept an event
parameter and call event.preventDefault() before running existing checks
(isFormInvalid/isPending) and logic; apply the same change to the other similar
block referenced (lines 131-137) so both flows use <form onSubmit> +
type="submit" instead of click handlers.
apps/customer/src/app/login/_components/LoginForm.tsx (1)

28-45: email/password 입력에 autoComplete를 추가해 주세요

브라우저 자동완성/비밀번호 매니저 연동을 위해 email, current-password 지정이 있으면 UX가 더 좋아집니다.

수정 제안
         <Input
           type="email"
           placeholder={LOGIN_TEXT.emailPlaceholder}
           value={values.email}
+          autoComplete="email"
           onChange={(e) => onChangeEmail(e.target.value)}
         />
@@
         <Input
           type="password"
           placeholder={LOGIN_TEXT.passwordPlaceholder}
           value={values.password}
+          autoComplete="current-password"
           onChange={(e) => onChangePassword(e.target.value)}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/_components/LoginForm.tsx` around lines 28 - 45,
Add browser autocomplete attributes to the email and password Input components
in LoginForm.tsx: set the email Input's autoComplete to "email" and the password
Input's autoComplete to "current-password" so password managers and browser
autofill work properly; update the two Input elements (the one using
values.email and onChangeEmail, and the one using values.password and
onChangePassword) to include these attributes while keeping existing props
(placeholder, value, onChange).
apps/customer/src/app/login/page.tsx (2)

70-72: 카카오 로그인 미구현 상태

handleKakaoLoginconsole.log만 호출하는 플레이스홀더입니다. 의도된 것이라면 TODO 주석을 추가하거나, 버튼을 비활성화하는 것이 좋습니다.

구현 예정이라면 TODO 주석을 추가하시겠습니까? 또는 카카오 OAuth 연동 코드를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 70 - 72, handleKakaoLogin
is currently a placeholder that only logs to console; either implement the Kakao
OAuth flow or make the placeholder explicit and safe. Add a TODO comment inside
handleKakaoLogin noting the planned implementation, and update the corresponding
Kakao login button to be disabled (e.g., disabled and aria-disabled) until real
OAuth is implemented so users can't trigger a no-op; alternatively, if you
prefer immediate implementation, replace handleKakaoLogin with the Kakao OAuth
initiation (popup/redirect) and proper success/error handling. Ensure you modify
the handleKakaoLogin function and the Kakao login button component referenced in
the login page.

25-37: 선택적 개선: 중복 핸들러 통합 가능

handleChangeEmailhandleChangePassword는 동일한 패턴을 따르므로, 제네릭 핸들러로 통합할 수 있습니다.

♻️ 제네릭 핸들러로 리팩토링 제안
-  const handleChangeEmail = (value: string) => {
-    setFormValues((prev) => ({
-      ...prev,
-      email: value,
-    }));
-  };
-
-  const handleChangePassword = (value: string) => {
-    setFormValues((prev) => ({
-      ...prev,
-      password: value,
-    }));
-  };
+  const handleChangeField = (field: keyof LoginReqDTO) => (value: string) => {
+    setFormValues((prev) => ({
+      ...prev,
+      [field]: value,
+    }));
+  };

JSX에서 사용:

onChangeEmail={handleChangeField("email")}
onChangePassword={handleChangeField("password")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 25 - 37, handleChangeEmail
and handleChangePassword are duplicate setters; replace them with a generic
factory like handleChangeField(fieldName) that returns a handler which calls
setFormValues(prev => ({ ...prev, [fieldName]: value })); update usages (e.g.,
onChangeEmail and onChangePassword) to call handleChangeField("email") and
handleChangeField("password") respectively and remove the two specific functions
(references: handleChangeEmail, handleChangePassword, setFormValues).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/customer/src/app/login/_components/LoginForm.tsx`:
- Around line 49-55: The LoginForm currently handles submission via Button
type="button" and onClick={onSubmit}, so pressing Enter in inputs won't submit;
modify the LoginForm component to wrap the username/password inputs in a <form>
and move the submission handler to the form (e.g., onSubmit={onSubmit}), then
change the Button in this diff from type="button" to type="submit" (keep
disabled={isPending}); alternatively you may add an onKeyDown Enter handler on
the input elements, but prefer the standard form approach so the existing
onSubmit handler is used by the form submission.

In `@apps/customer/src/app/login/page.tsx`:
- Around line 63-65: The onError handler in apps/customer/src/app/login/page.tsx
currently only console.logs the error; replace that with user-facing feedback by
updating the onError: (error) => { ... } handler to surface a clear message
(e.g., show a toast or set a local error state used by the UI) and map common
error cases (invalid credentials vs network/timeout vs unknown) to friendly
messages; use the existing toast/toastHook or a setState like loginError and
ensure the message includes error.message when appropriate and is cleared on
retry or success.
- Around line 43-46: Replace the console.log in the empty-field check with a
user-facing notification so users get immediate feedback: where the code checks
if (!email || !password) and currently calls
console.log(LOGIN_TEXT.emptyFieldMessage) (likely inside your submit/handleLogin
flow in page.tsx), call the app's UI feedback mechanism instead (e.g., show a
Toast, alert, or set an inline error state/variable bound to the form) using
LOGIN_TEXT.emptyFieldMessage so the message is visible to users and prevent
submission by returning as before.

In `@apps/customer/src/app/signup/page.tsx`:
- Around line 56-60: setSubmitError currently assigns error.message directly
which exposes internal errors; instead update the signup error handling in
page.tsx (where setSubmitError is called) to map known Error types/messages to
fixed user-facing strings and fall back to a single generic message like "회원가입 중
오류가 발생했습니다." Do this by inspecting the thrown value (e.g., error instanceof
Error ? error.message : ...) and translating it via a small switch/map before
calling setSubmitError, ensuring no raw error.message is passed through.
- Around line 44-50: Validation trims the inputs but the signUp call sends
original values, causing possible data mismatch; update the signUp invocation
(the call to signUp in page.tsx) to send the normalized/trimmed versions of the
fields (e.g., use trimmedName, trimmedNickname, trimmedEmail or call .trim() on
name, nickname, email before passing) so the transmitted payload matches the
earlier trim-based validation. Ensure the same normalization is applied to all
fields validated (memberName/name, nickname, email, etc.) and reuse the
normalized variables rather than the raw inputs.

In `@apps/customer/src/shared/api/api.ts`:
- Around line 10-31: The tokenStore implementation currently reads/writes access
and refresh tokens to localStorage (tokenStore, getAccessToken, getRefreshToken,
setTokens, clearTokens), which exposes them to XSS; change the flow so refresh
tokens are not stored in localStorage but instead are issued/cleared by the
server as HttpOnly Secure cookies (client JS must not read them), and keep
access tokens out of persistent storage (store in memory or short-lived
non-persistent storage) or use SameSite/HttpOnly cookies for both if moving
fully to cookie-based auth; update tokenStore methods: remove any client-side
reads/writes of the refresh token (getRefreshToken should stop returning
localStorage value and return null/undefined or be removed), make setTokens
avoid persisting refreshToken to localStorage (only set access token in memory),
and make clearTokens call a logout/clear-cookie endpoint on the server rather
than removing an HttpOnly cookie directly, plus update any code paths that
depended on localStorage refresh reads to use the server-side silent refresh
endpoint that relies on the HttpOnly cookie.
- Around line 34-37: The current compasserApi initialization uses
process.env.NEXT_PUBLIC_API_BASE_URL ?? "" which falls back to an empty string
and causes same-origin requests if the env is missing; change initialization to
fail-fast by validating NEXT_PUBLIC_API_BASE_URL before calling
createCompasserApi (e.g., throw an Error or assert) and pass the verified
non-empty baseURL into createCompasserApi (reference compasserApi and
createCompasserApi to locate the code); ensure the error message clearly states
the missing NEXT_PUBLIC_API_BASE_URL so startup fails immediately instead of
silently using an empty baseURL.

In `@apps/owner/src/app/`(tabs)/mypage/store-info/page.tsx:
- Around line 65-66: selectedTag (and tagOptions/setSelectedTag) is only used
for UI state and never sent to the server, so tag changes don't persist; update
the store-save flow (the form submit or save handler such as the function that
builds the save payload — e.g., handleSave/onSubmit/buildPayload) to include the
selectedTag value in the request payload by mapping the UI value to the API
field (e.g., { tag: selectedTag } or the API's expected enum/key), and ensure
the component initializes selectedTag from the server value as currently done;
if the backend API does not accept tag updates, hide or disable the tag selector
UI instead.
- Around line 57-63: The four fields storeEmail, bankName, depositor, and
bankAccount are initialized to empty strings and not seeded from myStore, so
saving can overwrite existing data with blanks; update the initialization to
derive initial state from myStore (e.g., useState(myStore?.storeEmail ?? "")
etc. for storeEmail, bankName, depositor, bankAccount) and/or change the save
handler (the function that builds the payload in the save/onSubmit handler) to
fall back to myStore values when the corresponding state is empty (e.g.,
payload.field = stateField || myStore?.field) so existing values aren’t lost;
adjust both the state declarations (storeEmail, bankName, depositor,
bankAccount) and the save payload construction to reference myStore where
appropriate.

In `@apps/owner/src/app/signup/page.tsx`:
- Around line 39-40: The code currently mutates the submitted credentials by
using formValues.password.trim() and formValues.passwordConfirm.trim(); instead,
keep the raw values (e.g., const password = formValues.password; const
passwordConfirm = formValues.passwordConfirm;) and create separate trimmed
versions only for validation (e.g., const passwordTrimmed = password.trim()).
Use the trimmed variables to perform empty/whitespace checks but send the
original password and passwordConfirm when constructing the payload or calling
the signup function (refer to password, passwordConfirm, passwordTrimmed,
formValues, and the signup handler in page.tsx); apply the same change to the
other similar blocks noted (lines 42-49, 53-60).
- Around line 42-50: The current checks that use console.log for missing fields
and API failures (references: memberName, nickname, email, password,
passwordConfirm and the signup submit handler where console.log is used) must be
replaced with a user-facing error state and display: add a local state like
errorMessage (or use a toast utility), set a clear, user-friendly message when
validation fails (e.g., "모든 항목을 입력해주세요.") instead of console.log, render that
message inline near the form or trigger a toast, and for API errors catch the
error, map it to a sanitized user message and set errorMessage (do not expose
raw error objects to users); keep only sanitized info in console logs for
debugging. Ensure both the client-side validation branch and the API catch block
update and display the same error UI.

In `@apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx`:
- Around line 62-69: handleSubmit currently fires onSubmit without guarding
against concurrent calls or rejections; add local isSubmitting and error state,
return early if isSubmitting is true, set isSubmitting = true before awaiting
onSubmit(form), wrap the await in try/catch to capture and set error state on
rejection, call onClose() only on successful completion, and in finally set
isSubmitting = false; apply the same change to the other submit handler around
lines 153-160 to prevent double submits and unhandled rejections.

In `@apps/owner/src/app/signup/register/_utils/business-hours.ts`:
- Around line 15-17: parseBusinessHours currently returns the shared
EMPTY_BUSINESS_HOURS object when input is empty, which reuses the same reference
across calls; instead, return a fresh copy to avoid shared-reference bugs in
React state and accidental mutations. Update parseBusinessHours to create and
return a new BusinessHoursValue (e.g., shallow/deep clone or construct a new
object with the same fields) rather than returning EMPTY_BUSINESS_HOURS
directly; ensure any nested arrays/objects are also copied if they can be
mutated so the returned value is independent of the constant.

In `@apps/owner/src/app/signup/register/page.tsx`:
- Around line 61-62: The selectedTag state (tagOptions, selectedTag,
setSelectedTag) is only kept in local component state and never persisted or
used in handleCompleteRegister, so the chosen tag never reaches the server;
either remove the tag UI or persist it by initializing selectedTag from existing
user/profile data on mount and include selectedTag in the registration payload
inside handleCompleteRegister (map the literal value to the server field
expected), and ensure any update paths (e.g., post-register patch or create-user
API call) save it so the value is restored on re-entry.

In `@apps/owner/src/shared/api/api.ts`:
- Around line 21-29: The setTokens function (TokenPair) leaves an old
ownerRefreshToken in localStorage when refreshToken is absent; update setTokens
so after setting ownerAccessToken it explicitly removes ownerRefreshToken
(localStorage.removeItem("ownerRefreshToken")) when refreshToken is falsy,
preserving the existing typeof window guard and current ownerAccessToken write
path to maintain consistency between tokens.
- Around line 38-40: The code silently allows an empty API base URL by
defaulting NEXT_PUBLIC_API_BASE_URL to "", so update the compasserApi
initialization to validate the environment variable and fail fast: in the
createCompasserApi call (compasserApi, createCompasserApi, tokenStore) check
process.env.NEXT_PUBLIC_API_BASE_URL is present and non-empty at startup and
throw or log a fatal error if missing (do not proceed with ""), so the app fails
loudly during initialization rather than making relative-path requests at
runtime.

In `@apps/owner/src/shared/stores/ownerSignup.store.ts`:
- Line 13: The store currently persists the email property to localStorage which
exposes PII; update the persist configuration used to create the owner signup
store (the persist(...) call that wraps this store) to exclude the email field
by using the partialize option and only persist non-PII progress/state keys,
keeping email as a normal in-memory property; apply the same change for the
other persisted store slice referenced around the code handling the fields at
61-63 so email is never written to storage.

In `@apps/owner/src/shared/utils/businessLicense.ts`:
- Around line 5-10: isValidBusinessNumber currently duplicates
isValidBusinessNumberFormat and only checks for 10 digits; replace its body to
perform the actual business number checksum validation (or remove it and use a
single function). Specifically, keep normalizeBusinessNumber and
isValidBusinessNumberFormat for format checking, and update
isValidBusinessNumber to: after normalizing and format-checking the value,
compute the checksum using the standard 10-digit business number algorithm (use
weights [1,3,7,1,3,7,1,3,5], add the carry from the 9th digit multiplication as
floor((digit9*5)/10), sum all products, then verify total % 10 === 0) so only
numbers that pass the checksum return true.

---

Outside diff comments:
In
`@apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx`:
- Around line 46-68: The delete <button> is nested inside the upload <button>
(the element using handleOpenFilePicker/previewUrl/Image), which violates HTML
specs; refactor so the delete control is not a child of the upload button: move
the delete control (currently rendering Icon and using handleClickRemove) out to
be a sibling element (e.g., a positioned <button> or <div role="button">)
adjacent to the upload <button>, preserve its styling and aria-label="사진 삭제",
and keep any event handlers (handleClickRemove) but remove reliance on
event.stopPropagation() to prevent nested-button issues; ensure keyboard focus
and onKey handlers remain functional for the new control.

---

Nitpick comments:
In `@apps/customer/src/app/login/_components/LoginForm.tsx`:
- Around line 28-45: Add browser autocomplete attributes to the email and
password Input components in LoginForm.tsx: set the email Input's autoComplete
to "email" and the password Input's autoComplete to "current-password" so
password managers and browser autofill work properly; update the two Input
elements (the one using values.email and onChangeEmail, and the one using
values.password and onChangePassword) to include these attributes while keeping
existing props (placeholder, value, onChange).

In `@apps/customer/src/app/login/page.tsx`:
- Around line 70-72: handleKakaoLogin is currently a placeholder that only logs
to console; either implement the Kakao OAuth flow or make the placeholder
explicit and safe. Add a TODO comment inside handleKakaoLogin noting the planned
implementation, and update the corresponding Kakao login button to be disabled
(e.g., disabled and aria-disabled) until real OAuth is implemented so users
can't trigger a no-op; alternatively, if you prefer immediate implementation,
replace handleKakaoLogin with the Kakao OAuth initiation (popup/redirect) and
proper success/error handling. Ensure you modify the handleKakaoLogin function
and the Kakao login button component referenced in the login page.
- Around line 25-37: handleChangeEmail and handleChangePassword are duplicate
setters; replace them with a generic factory like handleChangeField(fieldName)
that returns a handler which calls setFormValues(prev => ({ ...prev,
[fieldName]: value })); update usages (e.g., onChangeEmail and onChangePassword)
to call handleChangeField("email") and handleChangeField("password")
respectively and remove the two specific functions (references:
handleChangeEmail, handleChangePassword, setFormValues).

In `@apps/customer/src/app/signup/page.tsx`:
- Around line 38-39: Convert the click-only signup flow to a proper form
submission: wrap the inputs and button in a <form> with onSubmit tied to
handleSignup (replace button onClick), change the button to type="submit", and
update handleSignup to accept an event parameter and call event.preventDefault()
before running existing checks (isFormInvalid/isPending) and logic; apply the
same change to the other similar block referenced (lines 131-137) so both flows
use <form onSubmit> + type="submit" instead of click handlers.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0465f3e3-38b2-45c3-8668-36f2be3f0e05

📥 Commits

Reviewing files that changed from the base of the PR and between 776a49a and 94924b8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (42)
  • apps/customer/src/app/login/_components/KakaoLoginButton.tsx
  • apps/customer/src/app/login/_components/LoginForm.tsx
  • apps/customer/src/app/login/_components/SignupLinkButton.tsx
  • apps/customer/src/app/login/_constants/login.constants.ts
  • apps/customer/src/app/login/page.tsx
  • apps/customer/src/app/signup/page.tsx
  • apps/customer/src/shared/api/api.ts
  • apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts
  • apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts
  • apps/owner/src/app/(tabs)/mypage/store-info/page.tsx
  • apps/owner/src/app/signup/business/page.tsx
  • apps/owner/src/app/signup/page.tsx
  • apps/owner/src/app/signup/register/_components/fields/AccountField.tsx
  • apps/owner/src/app/signup/register/_components/fields/EmailField.tsx
  • apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx
  • apps/owner/src/app/signup/register/_components/fields/StoreNameField.tsx
  • apps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsx
  • apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx
  • apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
  • apps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsx
  • apps/owner/src/app/signup/register/_components/sections/TagSection.tsx
  • apps/owner/src/app/signup/register/_constants/register.ts
  • apps/owner/src/app/signup/register/_types/register.ts
  • apps/owner/src/app/signup/register/_utils/business-hours.ts
  • apps/owner/src/app/signup/register/page.tsx
  • apps/owner/src/shared/api/api.ts
  • apps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.ts
  • apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts
  • apps/owner/src/shared/queries/query/useMyStoreQuery.ts
  • apps/owner/src/shared/queries/query/useRandomBoxListQuery.ts
  • apps/owner/src/shared/queries/query/useStoreImageQuery.ts
  • apps/owner/src/shared/stores/ownerSignup.store.ts
  • apps/owner/src/shared/utils/bank.ts
  • apps/owner/src/shared/utils/businessLicense.ts
  • package.json
💤 Files with no reviewable changes (2)
  • apps/owner/src/app/signup/register/_types/register.ts
  • apps/owner/src/app/signup/register/_constants/register.ts

Comment on lines +49 to +55
<Button
type="button"
size="lg"
kind="default"
variant="primary"
onClick={onSubmit}
disabled={isPending}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# LoginForm 사용처에서 form submit 연결 여부 확인
rg -n -C3 'LoginForm' apps/customer/src/app/login
rg -n -C3 '<form|onSubmit=|type="submit"' apps/customer/src/app/login

Repository: CUK-Compasser/FE

Length of output: 3382


🏁 Script executed:

cat -n apps/customer/src/app/login/_components/LoginForm.tsx

Repository: CUK-Compasser/FE

Length of output: 2103


🏁 Script executed:

cat -n apps/customer/src/app/login/page.tsx | head -100

Repository: CUK-Compasser/FE

Length of output: 3148


🏁 Script executed:

rg -n 'form|Form' apps/customer/src/app/login/_components/LoginForm.tsx

Repository: CUK-Compasser/FE

Length of output: 151


LoginForm이 Enter 키 제출을 지원하지 않습니다

현재 구현에서 LoginForm은 <form> 요소로 감싸져 있지 않고, Button의 type="button"onClick={onSubmit} 조합으로만 제출을 처리합니다. 따라서 비밀번호 입력 필드에서 Enter 키를 눌러도 폼이 제출되지 않으며, 오직 버튼 클릭으로만 로그인이 가능합니다.

표준 HTML 폼 동작에 대한 사용자 기대치를 벗어나므로 다음 중 하나로 개선이 필요합니다:

  • <form> 요소로 inputs을 감싸고 Button의 type="submit"으로 변경
  • 또는 Input 요소들에 Enter 키 이벤트 핸들러(onKeyDown) 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/_components/LoginForm.tsx` around lines 49 - 55,
The LoginForm currently handles submission via Button type="button" and
onClick={onSubmit}, so pressing Enter in inputs won't submit; modify the
LoginForm component to wrap the username/password inputs in a <form> and move
the submission handler to the form (e.g., onSubmit={onSubmit}), then change the
Button in this diff from type="button" to type="submit" (keep
disabled={isPending}); alternatively you may add an onKeyDown Enter handler on
the input elements, but prefer the standard form approach so the existing
onSubmit handler is used by the form submission.

Comment on lines +43 to +46
if (!email || !password) {
console.log(LOGIN_TEXT.emptyFieldMessage);
return;
}
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

사용자에게 유효성 검사 실패 피드백 필요

console.log는 개발 중에만 확인 가능하며, 실제 사용자는 이메일/비밀번호가 비어있을 때 아무런 피드백을 받지 못합니다. Toast, alert, 또는 inline error message를 통해 사용자에게 알려야 합니다.

🛠️ 사용자 피드백 추가 제안
    if (!email || !password) {
-     console.log(LOGIN_TEXT.emptyFieldMessage);
+     // TODO: Toast 또는 alert로 사용자에게 피드백 제공
+     alert(LOGIN_TEXT.emptyFieldMessage);
      return;
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 43 - 46, Replace the
console.log in the empty-field check with a user-facing notification so users
get immediate feedback: where the code checks if (!email || !password) and
currently calls console.log(LOGIN_TEXT.emptyFieldMessage) (likely inside your
submit/handleLogin flow in page.tsx), call the app's UI feedback mechanism
instead (e.g., show a Toast, alert, or set an inline error state/variable bound
to the form) using LOGIN_TEXT.emptyFieldMessage so the message is visible to
users and prevent submission by returning as before.

Comment on lines +63 to +65
onError: (error) => {
console.log("일반 로그인 실패", error);
},
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

로그인 실패 시 사용자 피드백 필요

로그인 실패 시 console.log만 호출되어 사용자는 실패 원인을 알 수 없습니다. 잘못된 자격 증명, 네트워크 오류 등 상황에 맞는 에러 메시지를 사용자에게 표시해야 합니다.

🛠️ 에러 피드백 추가 제안
        onError: (error) => {
-         console.log("일반 로그인 실패", error);
+         console.error("일반 로그인 실패", error);
+         // TODO: 사용자에게 에러 메시지 표시
+         alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
        },
📝 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
onError: (error) => {
console.log("일반 로그인 실패", error);
},
onError: (error) => {
console.error("일반 로그인 실패", error);
// TODO: 사용자에게 에러 메시지 표시
alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 63 - 65, The onError
handler in apps/customer/src/app/login/page.tsx currently only console.logs the
error; replace that with user-facing feedback by updating the onError: (error)
=> { ... } handler to surface a clear message (e.g., show a toast or set a local
error state used by the UI) and map common error cases (invalid credentials vs
network/timeout vs unknown) to friendly messages; use the existing
toast/toastHook or a setState like loginError and ensure the message includes
error.message when appropriate and is cleared on retry or success.

Comment on lines +44 to +50
await signUp({
memberName: name,
nickname,
email,
password,
passwordConfirm,
});
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

검증은 trim인데 전송은 원본 값이라 데이터 불일치가 발생할 수 있습니다

Line 29-36에서 trim 검증을 하므로, Line 44-49 전송값도 동일하게 정규화해 보내는 편이 안전합니다.

수정 제안
       await signUp({
-        memberName: name,
-        nickname,
-        email,
+        memberName: name.trim(),
+        nickname: nickname.trim(),
+        email: email.trim(),
         password,
         passwordConfirm,
       });
📝 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
await signUp({
memberName: name,
nickname,
email,
password,
passwordConfirm,
});
await signUp({
memberName: name.trim(),
nickname: nickname.trim(),
email: email.trim(),
password,
passwordConfirm,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/signup/page.tsx` around lines 44 - 50, Validation trims
the inputs but the signUp call sends original values, causing possible data
mismatch; update the signUp invocation (the call to signUp in page.tsx) to send
the normalized/trimmed versions of the fields (e.g., use trimmedName,
trimmedNickname, trimmedEmail or call .trim() on name, nickname, email before
passing) so the transmitted payload matches the earlier trim-based validation.
Ensure the same normalization is applied to all fields validated
(memberName/name, nickname, email, etc.) and reuse the normalized variables
rather than the raw inputs.

Comment on lines +56 to +60
setSubmitError(
error instanceof Error
? error.message
: "회원가입 중 오류가 발생했습니다.",
);
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

원본 에러 메시지 직접 노출은 피하는 것이 좋습니다

Line 56-60에서 error.message를 그대로 보여주면 내부 메시지가 사용자에게 노출될 수 있습니다. 사용자 메시지는 고정/매핑 기반으로 제한하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/signup/page.tsx` around lines 56 - 60, setSubmitError
currently assigns error.message directly which exposes internal errors; instead
update the signup error handling in page.tsx (where setSubmitError is called) to
map known Error types/messages to fixed user-facing strings and fall back to a
single generic message like "회원가입 중 오류가 발생했습니다." Do this by inspecting the
thrown value (e.g., error instanceof Error ? error.message : ...) and
translating it via a small switch/map before calling setSubmitError, ensuring no
raw error.message is passed through.

Comment on lines +61 to +62
const tagOptions = ["카페", "베이커리", "식당"] as const;
const [selectedTag, setSelectedTag] = useState<"" | "카페" | "베이커리" | "식당">("");
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

회원가입 완료 시 선택한 태그가 전혀 저장되지 않습니다.

selectedTag는 로컬 state로만 유지되고, 재진입 시에도 복원되지 않으며, handleCompleteRegister에서도 사용되지 않습니다. 지금 UI에서는 태그를 고를 수 있지만 서버 데이터에는 반영되지 않습니다. 등록 단계에서 필요 없는 정보라면 섹션을 제거하고, 필요하다면 저장 payload에 매핑해 주세요.

Also applies to: 71-78, 188-199, 255-259

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/app/signup/register/page.tsx` around lines 61 - 62, The
selectedTag state (tagOptions, selectedTag, setSelectedTag) is only kept in
local component state and never persisted or used in handleCompleteRegister, so
the chosen tag never reaches the server; either remove the tag UI or persist it
by initializing selectedTag from existing user/profile data on mount and include
selectedTag in the registration payload inside handleCompleteRegister (map the
literal value to the server field expected), and ensure any update paths (e.g.,
post-register patch or create-user API call) save it so the value is restored on
re-entry.

Comment on lines +21 to +29
setTokens: ({ accessToken, refreshToken }: TokenPair) => {
if (typeof window === "undefined") return;

localStorage.setItem("ownerAccessToken", accessToken);

if (refreshToken) {
localStorage.setItem("ownerRefreshToken", refreshToken);
}
},
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 | 🔴 Critical

refreshToken 누락 시 이전 토큰이 남는 결함이 있습니다.

현재는 refreshToken이 없을 때 기존 ownerRefreshToken을 삭제하지 않아, 이전 세션 토큰이 잔존할 수 있습니다. 토큰 쌍 일관성이 깨져 인증 오류/오염 상태를 유발할 수 있습니다.

수정 예시
   setTokens: ({ accessToken, refreshToken }: TokenPair) => {
     if (typeof window === "undefined") return;

     localStorage.setItem("ownerAccessToken", accessToken);

     if (refreshToken) {
       localStorage.setItem("ownerRefreshToken", refreshToken);
+    } else {
+      localStorage.removeItem("ownerRefreshToken");
     }
   },
📝 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
setTokens: ({ accessToken, refreshToken }: TokenPair) => {
if (typeof window === "undefined") return;
localStorage.setItem("ownerAccessToken", accessToken);
if (refreshToken) {
localStorage.setItem("ownerRefreshToken", refreshToken);
}
},
setTokens: ({ accessToken, refreshToken }: TokenPair) => {
if (typeof window === "undefined") return;
localStorage.setItem("ownerAccessToken", accessToken);
if (refreshToken) {
localStorage.setItem("ownerRefreshToken", refreshToken);
} else {
localStorage.removeItem("ownerRefreshToken");
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/shared/api/api.ts` around lines 21 - 29, The setTokens
function (TokenPair) leaves an old ownerRefreshToken in localStorage when
refreshToken is absent; update setTokens so after setting ownerAccessToken it
explicitly removes ownerRefreshToken
(localStorage.removeItem("ownerRefreshToken")) when refreshToken is falsy,
preserving the existing typeof window guard and current ownerAccessToken write
path to maintain consistency between tokens.

Comment on lines +38 to +40
export const compasserApi = createCompasserApi({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
tokenStore,
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

API baseURL 미설정을 조용히 허용하지 마세요.

NEXT_PUBLIC_API_BASE_URL이 없을 때 ""로 진행하면 상대 경로 호출로 이어져 환경 오설정이 런타임에서 늦게 드러납니다. 초기화 시점에 명시적으로 실패시키는 편이 안전합니다.

수정 예시
-export const compasserApi = createCompasserApi({
-  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
-  tokenStore,
-});
+const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL;
+if (!baseURL) {
+  throw new Error("NEXT_PUBLIC_API_BASE_URL is required");
+}
+
+export const compasserApi = createCompasserApi({
+  baseURL,
+  tokenStore,
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/shared/api/api.ts` around lines 38 - 40, The code silently
allows an empty API base URL by defaulting NEXT_PUBLIC_API_BASE_URL to "", so
update the compasserApi initialization to validate the environment variable and
fail fast: in the createCompasserApi call (compasserApi, createCompasserApi,
tokenStore) check process.env.NEXT_PUBLIC_API_BASE_URL is present and non-empty
at startup and throw or log a fatal error if missing (do not proceed with ""),
so the app fails loudly during initialization rather than making relative-path
requests at runtime.

signupCompleted: boolean;
businessCompleted: boolean;
registerCompleted: boolean;
email: string | null;
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

이메일을 영구 저장하지 않는 쪽이 안전합니다.

현재 emailpersist 대상이라 localStorage에 남습니다. 공유 기기/장기 잔존 관점에서 PII 노출 위험이 있어, partialize로 진행 상태만 저장하고 이메일은 메모리 상태로 유지하는 구성이 더 안전합니다.

수정 예시
 export const useOwnerSignupStore = create<OwnerSignupState>()(
   persist(
     (set) => ({
       step: "signup",
       signupCompleted: false,
       businessCompleted: false,
       registerCompleted: false,
       email: null,
@@
     {
       name: "owner-signup-flow",
+      partialize: (state) => ({
+        step: state.step,
+        signupCompleted: state.signupCompleted,
+        businessCompleted: state.businessCompleted,
+        registerCompleted: state.registerCompleted,
+      }),
     },
   ),
 );

Also applies to: 61-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/shared/stores/ownerSignup.store.ts` at line 13, The store
currently persists the email property to localStorage which exposes PII; update
the persist configuration used to create the owner signup store (the
persist(...) call that wraps this store) to exclude the email field by using the
partialize option and only persist non-PII progress/state keys, keeping email as
a normal in-memory property; apply the same change for the other persisted store
slice referenced around the code handling the fields at 61-63 so email is never
written to storage.

Comment on lines +5 to +10
export const isValidBusinessNumberFormat = (value: string) => {
return /^\d{10}$/.test(normalizeBusinessNumber(value));
};

export const isValidBusinessNumber = (value: string) => {
return /^\d{10}$/.test(normalizeBusinessNumber(value));
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

isValidBusinessNumber가 현재는 형식 검사와 동일합니다.

이 구현이면 사업자 인증 페이지의 두 번째 검증 분기가 사실상 도달 불가입니다. 지금은 10자리 숫자이기만 하면 모두 통과하므로, 실제 유효성 검사(체크섬)를 넣거나 함수 하나로 정리해야 합니다.

수정 예시
+const BUSINESS_NUMBER_REGEX = /^\d{10}$/;
+
 export const normalizeBusinessNumber = (value: string) => {
   return value.replace(/\D/g, "");
 };
 
 export const isValidBusinessNumberFormat = (value: string) => {
-  return /^\d{10}$/.test(normalizeBusinessNumber(value));
+  return BUSINESS_NUMBER_REGEX.test(normalizeBusinessNumber(value));
 };
 
 export const isValidBusinessNumber = (value: string) => {
-  return /^\d{10}$/.test(normalizeBusinessNumber(value));
+  const normalized = normalizeBusinessNumber(value);
+  if (!BUSINESS_NUMBER_REGEX.test(normalized)) return false;
+
+  const digits = normalized.split("").map(Number);
+  const weights = [1, 3, 7, 1, 3, 7, 1, 3];
+  const weightedSum = weights.reduce(
+    (sum, weight, index) => sum + digits[index] * weight,
+    0,
+  );
+  const ninthProduct = digits[8] * 5;
+  const checksum =
+    (10 -
+      ((weightedSum +
+        Math.floor(ninthProduct / 10) +
+        (ninthProduct % 10)) %
+        10)) %
+    10;
+
+  return checksum === digits[9];
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/shared/utils/businessLicense.ts` around lines 5 - 10,
isValidBusinessNumber currently duplicates isValidBusinessNumberFormat and only
checks for 10 digits; replace its body to perform the actual business number
checksum validation (or remove it and use a single function). Specifically, keep
normalizeBusinessNumber and isValidBusinessNumberFormat for format checking, and
update isValidBusinessNumber to: after normalizing and format-checking the
value, compute the checksum using the standard 10-digit business number
algorithm (use weights [1,3,7,1,3,7,1,3,5], add the carry from the 9th digit
multiplication as floor((digit9*5)/10), sum all products, then verify total % 10
=== 0) so only numbers that pass the checksum return true.

@skyblue1232 skyblue1232 merged commit 108b1bf into develop Apr 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api 서버 - 클라이언트 통신 chore 자잘한 수정 feat 기능 구현 및 생성 refactor This doesn't seem right

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 사용자 회원가입, 로그인 API 연동

1 participant