Skip to content

Conversation

@SHIM-JINSEO
Copy link
Contributor

@SHIM-JINSEO SHIM-JINSEO commented Sep 9, 2025

  • 자신이 속한 그룹을 가져오는 feature가 추가되었습니다.
  • thirdpatry token 로직에 추가되었습니다.
  • 공지 작성시에 account를 선택하는 컴포넌트 아래에 groups client 로그인을 위한 버튼이 만들어졌습니다.(UI 수정 필요)
  • 그룹스에서 지글로 리다이렉트 시킬 때 authcode가 query에 있다면 localstorage에 저장되어 있던 URI로 이동합니다.
  • notice 데이터의 group 관련 field가 벡엔드에 맞추어 변경되었습니다.

Summary by CodeRabbit

  • New Features

    • 외부 서비스 OAuth 연동 도입 및 로그인 코드 처리와 자동 리다이렉트 페이지 추가
    • 내 그룹 불러오기, 그룹 선택 UI 및 그룹 기반 공지 작성 흐름 제공
    • 로그인 모달/버튼을 통한 그룹 로그인 UX 추가
  • Refactor

    • 계정 문자열 중심을 그룹 객체 중심으로 전면 전환 및 토큰 처리 단순화
    • 컴포넌트들이 전달된 그룹 정보를 직접 사용하도록 개선
  • Chores

    • 스테이징 그룹 API 도메인 업데이트
    • 저장 시 자동 포맷팅 활성화 및 기본 포매터 지정
    • 그룹 관련 안내문구(다국어) 추가

@SHIM-JINSEO SHIM-JINSEO requested a review from a team September 9, 2025 08:23
@SHIM-JINSEO SHIM-JINSEO self-assigned this Sep 9, 2025
@SHIM-JINSEO SHIM-JINSEO linked an issue Sep 9, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 9, 2025

📝 Walkthrough

Walkthrough

제3자(Third-party) 그룹 OAuth 인증을 도입하고, 그룹 식별자를 문자열(groupId)에서 객체(group { uuid, name, profileImageUrl })로 전환했다. 라우팅 콜백을 처리하는 클라이언트 컴포넌트를 추가하고, 관련 API 호출을 axios 기반으로 교체했으며 에디터/선택 UI, 제출 페이로드, 표시 컴포넌트를 일관되게 수정했다. 환경 변수 및 에디터 포맷 설정도 갱신되었다.

Changes

Cohort / File(s) Summary of changes
환경 및 에디터 설정
\.env, \.vscode/settings.json
NEXT_PUBLIC_GROUPS_API_URLhttps://api.stg.groups.gistory.me/로 변경. VSCode에서 저장 시 포맷팅 활성화(editor.formatOnSave: true) 및 Prettier 기본 포매터 지정.
그룹 API 리팩터링 & OAuth 연동
src/api/group/group.ts
ziggleApi 제거, axios 도입. thirdPartyAuth(), getGroupsToken(code), getUserInfo(accessToken) 추가 및 기존 getGroup을 axios로 전환. 새로운 타입(GroupInfoForUser, UserInfo, ThirdPartyGroup) 추가. 에러 핸들링/로그 추가.
공지 API 및 타입 변경
src/api/notice/notice.ts
NoticecreateNotice 페이로드에서 groupIdgroup(객체)로 변경. 토큰은 localStorage('groupsAccessToken')에서 동기적으로 읽음. 입력/응답의 그룹 식별자 필드명(UUID/ID 불일치 주의).
라우트 콜백 처리 컴포넌트 추가
src/app/[lng]/page.tsx, src/app/[lng]/thirdParty.tsx
서버사이드 리다이렉트 제거, ThirdParty 클라이언트 컴포넌트로 교체. 쿼리 code를 로컬스토리지에 저장하고 redirectPath 또는 /{lng}/home으로 클라이언트 리다이렉트 수행.
작성 흐름 — 상태/폼/제출 일괄 변경
src/app/[lng]/write/NoticeEditor.tsx, src/app/[lng]/write/handle-notice-submit.ts, src/app/[lng]/write/noticeEditorActions.ts
에디터 상태/액션/폼에서 account/groupId(문자열) → group 객체({ uuid, name, profileImageUrl })로 통일. 제출 시 payload에 group 포함. 초기 상태 및 reducer 액션 반영.
선택 UI 및 인증 흐름 변경
src/app/[lng]/write/SelectAccountArea.tsx
props/인터페이스를 accountgroup으로 변경. thirdPartyCode를 통해 getGroupsToken으로 토큰 교환, getUserInfo로 내 그룹 조회, 토큰/코드 로컬 저장, Swal을 통한 로그인 유도 및 redirectPath 보존 로직 추가.
렌더 컴포넌트 정합성
src/app/components/shared/Zabo/Zabo.tsx
비동기 getGroup 호출 제거. 전달된 group 객체로 프로필/이름 렌더링하도록 치환(후속 조회 제거).
국제화 문자열 추가
src/app/i18next/locales/en/translation.json, src/app/i18next/locales/ko/translation.json
그룹 로그인 안내 키 group.loginGroupsClient 추가(영/한).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant UI as 앱(UI)
  participant Auth as Groups Auth
  participant Local as localStorage
  participant API as Groups API

  rect rgba(230,245,255,0.6)
    User->>UI: "그룹으로 로그인" 클릭
    UI->>Local: redirectPath 저장
    UI->>Auth: thirdPartyAuth() → 브라우저 리다이렉트
  end

  User->>Auth: 인증/승인
  Auth-->>UI: 콜백 리디렉트 /[lng]?code=XYZ

  rect rgba(240,255,240,0.6)
    UI->>Local: code 저장('thirdPartyCode')
    UI->>UI: router.replace(redirectPath or /{lng}/home)
  end

  rect rgba(255,248,230,0.6)
    UI->>Local: thirdPartyCode 조회
    alt code 존재
      UI->>API: POST /third-party/token (code)
      API-->>UI: accessToken
      UI->>Local: groupsAccessToken 저장
      UI->>API: GET /third-party/userinfo (Bearer token)
      API-->>UI: myGroups
      UI-->>User: 그룹 목록 표시/선택
    else code 없음
      UI-->>User: 기존 상태(미로그인)
    end
  end
Loading
sequenceDiagram
  autonumber
  actor User as 사용자
  participant Editor as NoticeEditor
  participant Local as localStorage
  participant Backend as Backend API

  User->>Editor: 공지 작성 및 제출
  Editor->>Editor: noticeToSubmit 생성 (group 객체 포함)
  Editor->>Local: groupsAccessToken 조회(optional)
  Editor->>Backend: POST /notice { ..., group }
  Backend-->>Editor: 생성된 Notice (group: { id,... } | null)
  Editor-->>User: 완료 처리
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • dohyun-ko
  • ParkJumyung
  • 2paperstar
  • crown-3

Poem

깡충깡충 코드가 와르르, 토큰은 쏙 저장해
리디렉트 길도 기억해, 홈으로 쓱 돌아가네
그룹은 ID 대신 얼굴과 이름으로 빛나고
난 토끼, 바삭한 변화에 귀를 쫑긋해
퐁당! 공지 하나 띄우고 당근 한 입 먹는다 🥕

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[feat] get group contain me"은 PR의 핵심 변경사항(사용자가 속한 그룹을 불러오는 기능 추가 및 서드파티 인증 흐름 도입)과 관련이 있어 변경의 주제를 어느 정도 반영하고 있습니다. 다만 영어 표현이 부자연스럽고 요약으로서 가독성이 떨어져 팀이 히스토리를 빠르게 훑을 때 명확성이 낮습니다. 이에 따라 내용 연관성은 인정되지만 문구 개선이 권장됩니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 425-bug-get-group-contain-me

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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
Contributor

@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: 11

Caution

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

⚠️ Outside diff range comments (2)
src/api/notice/notice.ts (1)

107-123: Detail 페이지에서 groupId 대신 group 객체 사용으로 수정 필요

  • src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/NoticeInfo.tsxgroupId 필드를 group.uuid로 교체하고, 더 이상 불필요해진 getGroup(groupId) 호출을 제거하세요. 작성(handle-notice-submit.ts) 경로는 이미 group 객체를 사용 중입니다.
src/app/[lng]/write/handle-notice-submit.ts (1)

202-210: 영문 전용 공지 시 body가 undefined로 전송됩니다.

noticeLanguage === 'en'인 경우 koreanBody가 없어 body: koreanBody!가 런타임에서 빈 값이 됩니다. 서버가 필수로 body를 요구하면 바로 실패합니다. 아래처럼 언어에 따라 body를 선택하세요.

   const notice = await createNotice({
     title,
     deadline,
-    body: koreanBody!,
+    body: noticeLanguage === 'en'
+      ? (englishBody as string)
+      : (koreanBody as string),
     images: imageKeys,
     tags: tagIds,
     category,
     group,
   }).catch(() => null);
🧹 Nitpick comments (19)
.env (1)

6-6: Groups API Base URL의 트레일링 슬래시 제거 제안

경로 조합 시 //가 중복될 수 있어(예: ${BASE_URL}/v1...gistory.me//v1) 안정적으로 합치려면 트레일링 슬래시를 제거하는 편이 안전합니다.

다음 변경을 고려해 주세요:

-NEXT_PUBLIC_GROUPS_API_URL=https://api.stg.groups.gistory.me/
+NEXT_PUBLIC_GROUPS_API_URL=https://api.stg.groups.gistory.me

참고: dotenv-linter의 UnorderedKey 경고는 기능 영향은 없으며, 정렬은 팀 규칙에 맞춰 선택적으로 적용해도 됩니다.

.vscode/settings.json (1)

15-16: 포맷 온 세이브 활성화 OK — 팀 합의만 확인하면 됩니다

프로젝트 전반에 Prettier 포맷팅이 자동 적용됩니다. 에디터 확장(Prettier by esbenp)이 필요하므로 온보딩 가이드에 명시되면 좋겠습니다.

src/api/notice/notice.ts (2)

55-59: Notice 응답의 group.id와 생성 입력의 group.uuid 불일치

응답 타입은 group: { id: string }, 생성자 입력은 group: { uuid: string }로 달라 혼동/실수 여지가 큽니다. 동일한 도메인 개념이면 내부 타입을 통일하고, API 계약과 다르면 매핑 계층을 두는 편이 안전합니다.

한 가지 방식:

  • 내부 폼/상태에서는 id로 통일
  • 전송 시에만 uuid로 매핑

아래는 생성 요청 시 매핑하는 예시입니다(입력 타입을 id로 바꾸고 payload에서 uuid로 변환):

 export const createNotice = async ({
   title,
   deadline,
   body,
   images,
   tags,
   category,
-  group,
+  group,
 }: {
   title: string;
   deadline?: Date;
   body: string;
   images: string[];
   tags: number[];
   category: (typeof Category)[keyof typeof Category];
-  group: { uuid: string; name: string; profileImageUrl: string | null } | null;
+  group: { id: string; name: string; profileImageUrl: string | null } | null;
 }): Promise<NoticeDetail> => {
   const groupsToken: string | null = localStorage.getItem('groupsAccessToken');
   return ziggleApi
     .post(
       '/notice',
       {
         title,
         deadline,
         body,
         images,
         tags,
         category,
-        group: group ?? undefined,
+        group: group
+          ? {
+              uuid: group.id,
+              name: group.name,
+              profileImageUrl: group.profileImageUrl,
+            }
+          : undefined,
       },

또는 백엔드 계약이 허용한다면 응답 타입의 group.idgroup.uuid로 바꾸는 방법도 있습니다. 어느 쪽이 백엔드 표준인지 확인 부탁드립니다.


135-145: 헤더 주입 로직은 합리적 — 단, 미사용 import 정리 권장

토큰이 없을 때 헤더를 생략하는 분기 처리는 적절합니다. 다만 현재 파일 상단의 getGroupsToken import가 미사용으로 보입니다(린트 경고 유발). 제거해 주세요.

파일 상단에서 다음 줄을 삭제:

-import { getGroupsToken } from '../group/group';
src/app/[lng]/write/handle-notice-submit.ts (1)

25-29: Group 타입 중복 정의를 피하고 공용 타입으로 통합하세요.

여러 파일에서 { uuid; name; profileImageUrl } 형태를 반복 선언하고 있습니다. 공용 타입(예: GroupLite)을 한 곳에서 export해 사용하면 drift를 줄일 수 있습니다.

src/app/components/shared/Zabo/Zabo.tsx (2)

5-5: 미사용 import 제거.

getGroup를 더 이상 사용하지 않습니다. 제거하여 번들 크기와 혼동을 줄이세요.

-import { getGroup } from '@/api/group/group';

39-46: 디버그 로그 제거.

서버 컴포넌트에서 console.log는 서버 로그를 오염시킵니다. 필요 시 debug 플래그로 조건부 출력하세요.

-  console.log('group', group);
src/app/[lng]/page.tsx (1)

9-11: 불필요한 async 제거 및 다중 code 쿼리 대비.

async가 필요 없고, code가 배열로 들어오는 경우 첫 값을 안전히 사용하도록 처리하세요.

-export default async function Home({ params: { lng }, searchParams }: Props) {
-  const code = searchParams.code;
-  return <ThirdParty code={code as string} lng={lng} />;
+export default function Home({ params: { lng }, searchParams }: Props) {
+  const code = Array.isArray(searchParams.code)
+    ? searchParams.code[0]
+    : searchParams.code;
+  return <ThirdParty code={code as string} lng={lng} />;
src/app/[lng]/write/noticeEditorActions.ts (2)

25-29: Group 타입 중복 정의 통합 제안.

여기서도 { uuid; name; profileImageUrl }가 반복됩니다. 공용 타입으로 분리해 import하는 형태로 정리하면 안전합니다.

Also applies to: 45-46


65-73: 액션 명칭과 상태 필드의 불일치(SET_ACCOUNT→group).

액션 타입이 SET_ACCOUNT인 반면 상태는 group을 갱신합니다. 추후 혼선을 줄이기 위해 SET_GROUP 액션을 추가(또는 교체)하세요. 하위 호환을 위해 둘 다 처리하는 것을 권장합니다.

   | {
-      type: 'SET_ACCOUNT';
+      type: 'SET_ACCOUNT';
       group: {
         uuid: string;
         name: string;
         profileImageUrl: string | null;
       } | null;
     };
+  | {
+      type: 'SET_GROUP';
+      group: {
+        uuid: string;
+        name: string;
+        profileImageUrl: string | null;
+      } | null;
+    };
@@
-    case 'SET_ACCOUNT':
+    case 'SET_ACCOUNT':
+    case 'SET_GROUP':
       return { ...state, group: action.group };

Also applies to: 168-170

src/api/group/group.ts (2)

1-6: 불필요/부정확한 import 정리.

next/dist/server/api-utilsredirect는 잘못된 경로이고 사용되지도 않습니다. 또한 ziggleApi도 미사용입니다. 제거하세요.

-import axios from 'axios';
-import dayjs from 'dayjs';
-import { redirect } from 'next/dist/server/api-utils';
+import axios from 'axios';
+import dayjs from 'dayjs';
@@
-import { ziggleApi } from '..';
+// (no-op)

60-64: getGroup URL 정규화.

groupsUrl의 슬래시 유무와 무관하게 동작하도록 보정한 webBase를 사용하세요.

-export const getGroup = async (uuid: string): Promise<GroupInfo> => {
-  return axios
-    .get<GroupInfo>(`${groupsUrl}/group/${uuid}`)
+export const getGroup = async (uuid: string): Promise<GroupInfo> => {
+  return axios
+    .get<GroupInfo>(`${webBase}/group/${uuid}`)
     .then(({ data }) => data);
};
src/app/[lng]/write/SelectAccountArea.tsx (6)

4-4: 불필요한 import 제거 (use)

use는 사용되지 않습니다. 제거해 번들 크기와 린트 경고를 줄이세요.

-import { use, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';

34-35: 불리언 원시 타입 사용

Boolean 대신 원시 타입 boolean을 사용하세요. 박싱된 객체 타입은 예기치 않은 비교/진리값 문제를 유발할 수 있습니다.

-const [isModalOpen, setIsModalOpen] = useState<Boolean>(false);
+const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

64-78: 하드코딩된 텍스트 i18n 적용 + setState 중복 호출 정리

Swal 타이틀/본문은 i18n으로 치환하는 것이 좋습니다. 또한 confirm/else 모두에서 setIsModalOpen(false)가 호출되어 중복입니다.

-      Swal.fire({
-        title: 'Redirecting',
-        text: 'Do you want to get group information?',
+      Swal.fire({
+        title: t('groups.redirectingTitle'),
+        text: t('groups.redirectingBody'),
         icon: 'info',
         showCancelButton: true,
       }).then((result) => {
-        if (result.isConfirmed) {
-          thirdPartyAuth(path);
-          setIsModalOpen(false);
-        }
-        setIsModalOpen(false);
+        if (result.isConfirmed) {
+          thirdPartyAuth(path);
+        }
+        setIsModalOpen(false);
       });

키 값은 예시입니다. 실제 키는 프로젝트 컨벤션에 맞춰 주입해 주세요.


17-21: 중복 타입 정의 제거: 기존 GroupInfo 활용

동일 필드 서브셋이면 새 타입 정의 대신 Pick<GroupInfo, ...>를 사용해 단일 소스 유지가 좋습니다.

-type Group = {
-  uuid: string;
-  name: string;
-  profileImageUrl: string | null;
-};
+type Group = Pick<GroupInfo, 'uuid' | 'name' | 'profileImageUrl'>;

43-56: 사소한 정리: 불필요한 옵셔널 체이닝 제거

selectedGroup 존재가 보장된 이후에는 ?. 대신 직접 접근이 간결합니다.

-const groupData = {
-  uuid: selectedGroup?.uuid,
-  name: selectedGroup?.name,
-  profileImageUrl: selectedGroup?.profileImageUrl || null,
-};
+const groupData = {
+  uuid: selectedGroup.uuid,
+  name: selectedGroup.name,
+  profileImageUrl: selectedGroup.profileImageUrl ?? null,
+};

46-46: 디버그 로깅 제거 권장

console.log는 남기지 않는 것이 좋습니다. 필요 시 debug 플래그나 로거 유틸을 사용하세요.

-      console.log('selectedGroup', selectedGroup);
+      // no-op
-        console.log('data', data);
+        // no-op

Also applies to: 96-96

src/app/[lng]/write/NoticeEditor.tsx (1)

672-681: 타입 중복 최소화: 공용 타입/서브셋 사용

여기서도 인라인 객체 타입 대신 Pick<GroupInfo, 'uuid' | 'name' | 'profileImageUrl'> | null을 사용하면 중복을 줄일 수 있습니다. 액션 타입 명도 SET_GROUP으로 바꾸면 의도 전달이 더 명확합니다(옵션).

-            setGroup={(
-              group: {
-                uuid: string;
-                name: string;
-                profileImageUrl: string | null;
-              } | null,
-            ) => dispatch({ type: 'SET_ACCOUNT', group })}
+            setGroup={(group: Pick<GroupInfo, 'uuid' | 'name' | 'profileImageUrl'> | null) =>
+              dispatch({ type: 'SET_ACCOUNT', group })
+            }

추가 제안(별도 PR 권장):

  • 액션 타입 SET_ACCOUNTSET_GROUP 리네이밍.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91ca7fc and e524100.

📒 Files selected for processing (11)
  • .env (1 hunks)
  • .vscode/settings.json (1 hunks)
  • src/api/group/group.ts (2 hunks)
  • src/api/notice/notice.ts (3 hunks)
  • src/app/[lng]/page.tsx (1 hunks)
  • src/app/[lng]/thirdParty.tsx (1 hunks)
  • src/app/[lng]/write/NoticeEditor.tsx (2 hunks)
  • src/app/[lng]/write/SelectAccountArea.tsx (1 hunks)
  • src/app/[lng]/write/handle-notice-submit.ts (3 hunks)
  • src/app/[lng]/write/noticeEditorActions.ts (4 hunks)
  • src/app/components/shared/Zabo/Zabo.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/[lng]/write/SelectAccountArea.tsx (2)
src/app/i18next/index.ts (1)
  • PropsWithT (65-65)
src/api/group/group.ts (4)
  • GroupInfo (7-18)
  • thirdPartyAuth (29-32)
  • getGroupsToken (34-45)
  • getMyGroups (47-58)
src/app/[lng]/page.tsx (2)
src/app/i18next/index.ts (1)
  • PropsWithLng (66-66)
src/app/[lng]/thirdParty.tsx (1)
  • ThirdParty (7-16)
🪛 dotenv-linter (3.3.0)
.env

[warning] 6-6: [UnorderedKey] The NEXT_PUBLIC_GROUPS_API_URL key should go before the NEXT_PUBLIC_IDP_BASE_URL key

(UnorderedKey)

🔇 Additional comments (3)
src/app/[lng]/write/handle-notice-submit.ts (1)

45-47: 검증 완료: createNotice의 group 타입과 일치
createNotice 함수의 파라미터에서 group은 { uuid, name, profileImageUrl } | null로 정의되어 있어, 현재 전체 group 객체를 그대로 전달하는 방식이 타입 정의와 완전히 일치합니다. 별도 수정 불필요합니다.

src/app/[lng]/write/SelectAccountArea.tsx (1)

72-74: 리다이렉트 키(redirectPath)로 통일
thirdPartyAuth 함수에서 localStorage 키를 redirectPath로 변경해 전역 일관성을 유지하세요. 전체 코드베이스에서 해당 키 사용 현황을 검토해 다른 진입점에서 깨지지 않도록 확인해야 합니다.

src/app/[lng]/write/NoticeEditor.tsx (1)

229-231: group 페이로드 타입 일치 확인, 추가 검증 불필요
API createNoticegroup 파라미터 타입 { uuid, name, profileImageUrl } | null이 프론트엔드 state.group와 완전히 일치합니다.

groupsToken = await getGroupsToken();
}

const groupsToken: string | null = localStorage.getItem('groupsAccessToken');
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSR 안전성: API 레이어에서 localStorage 직접 접근

localStorage는 브라우저 전용입니다. API 모듈이 서버에서 호출될 가능성(예: Server Action/Route Handler)까지 고려하면 가드가 필요합니다.

-const groupsToken: string | null = localStorage.getItem('groupsAccessToken');
+const groupsToken: string | null =
+  typeof window !== 'undefined'
+    ? localStorage.getItem('groupsAccessToken')
+    : null;

대안: 토큰을 호출부(클라이언트)에서 인자로 주입하거나, 공용 토큰 리졸버 함수를 만들어 환경에 따라 안전하게 읽도록 분리하는 방법도 있습니다.

📝 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
const groupsToken: string | null = localStorage.getItem('groupsAccessToken');
// Guard against server-side execution: localStorage is only available in the browser
const groupsToken: string | null =
typeof window !== 'undefined'
? localStorage.getItem('groupsAccessToken')
: null;
🤖 Prompt for AI Agents
In src/api/notice/notice.ts around line 124, the code reads localStorage
directly which is unsafe for SSR; replace the direct access with a safe
approach: either accept the groupsAccessToken as a parameter from the caller
(preferred for API-layer purity) or call a shared token-resolver utility that
checks typeof window !== 'undefined' before reading localStorage and returns
null on server; update all callsites to pass the token when running on the
client or import the resolver and use it in place of localStorage.getItem to
avoid server-side errors.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (3)
src/app/[lng]/write/SelectAccountArea.tsx (2)

37-39: 렌더마다 반복 저장됨 — 의존성 배열 추가
useEffect에 의존성 미지정으로 매 렌더마다 redirectPath가 덮어써집니다. [path]를 지정하세요.

-  useEffect(() => {
-    localStorage.setItem('redirectPath', path);
-  });
+  useEffect(() => {
+    localStorage.setItem('redirectPath', path);
+  }, [path]);

81-95: 코드 교환 후 잔여 코드 제거
thirdPartyCode는 일회성입니다. 교환 성공 시 즉시 제거하세요(재시도 루프 방지).

   useEffect(() => {
     (async () => {
       if (thirdPartyCode) {
         const accessToken = await getGroupsToken(thirdPartyCode);
         if (accessToken) {
           localStorage.setItem('groupsAccessToken', accessToken);
           setAccessToken(accessToken);
+          localStorage.removeItem('thirdPartyCode');
         }
       }
     })();
   }, [thirdPartyCode]);
src/app/[lng]/thirdParty.tsx (1)

10-18: 오픈 리다이렉트 방지 + redirectPath 정리 누락
외부/절대 URL이 redirectPath에 들어오면 오픈 리다이렉트가 됩니다. 또한 사용 후 redirectPath를 지우지 않아 재사용 위험이 있습니다.

   useEffect(() => {
-    if (code) {
-      localStorage.setItem('thirdPartyCode', code);
-      const path = localStorage.getItem('redirectPath');
-      router.replace(path ? path : `/${lng}/home`);
-    } else {
-      router.replace(`/${lng}/home`);
-    }
+    const nextPath = localStorage.getItem('redirectPath');
+    if (code && typeof code === 'string') {
+      localStorage.setItem('thirdPartyCode', code);
+      const safePath =
+        nextPath && /^\/(?!\/)/.test(nextPath) ? nextPath : `/${lng}/home`;
+      router.replace(safePath);
+    } else {
+      router.replace(`/${lng}/home`);
+    }
+    localStorage.removeItem('redirectPath');
   }, [code, lng, router]);
🧹 Nitpick comments (8)
src/app/i18next/locales/ko/translation.json (1)

349-351: UI 문구 다듬기(자연스럽게)
표현을 간결하게 다듬으면 읽기성이 좋아집니다.

-    "loginGroupsClient": "그룹으로 공지를 작성하고 싶다면, 그룹스에 로그인해주세요"
+    "loginGroupsClient": "그룹으로 공지를 작성하려면 그룹스에 로그인해주세요"
src/app/i18next/locales/en/translation.json (1)

361-363: 영문 문구 자연스러움/관사 수정
"a notice"와 브랜드명 전치사 사용을 정리하면 더 자연스럽습니다.

-    "loginGroupsClient": "If you want to write notice as a group, please log in to the Groups"
+    "loginGroupsClient": "To write as a group, please log in to Groups"
src/api/group/group.ts (3)

68-72: 쿼리 파라미터 인코딩 방법 수정
파라미터 값에는 encodeURIComponent를 사용해야 안전합니다.

 export const thirdPartyAuth = async () => {
-  window.location.href = `${groupsUrl}/thirdParty?client_id=${encodeURI(
-    clientId!,
-  )}&redirect_uri=${encodeURI(redirectUri!)}`;
+  window.location.href = `${groupsUrl}/thirdParty?client_id=${encodeURIComponent(
+    clientId!
+  )}&redirect_uri=${encodeURIComponent(redirectUri!)}`;
 };

74-86: 반환 타입 명시로 사용처 안정화
명시적 반환 타입을 지정해 호출부에서 분기 처리(문자열/널)를 확실히 하세요.

-export const getGroupsToken = async (code: string) => {
+export const getGroupsToken = async (code: string): Promise<string | null> => {

88-100: 반환 타입 명시
동일하게 반환 타입을 명시해 주세요.

-export const getUserInfo = async (accessToken: string) => {
+export const getUserInfo = async (accessToken: string): Promise<UserInfo | null> => {
src/app/[lng]/write/SelectAccountArea.tsx (3)

34-34: 원시 타입 사용
Boolean 대신 boolean을 사용하세요.

-  const [isModalOpen, setIsModalOpen] = useState<Boolean>(false);
+  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

66-80: 모달 문구 i18n 적용 + 불필요 의존성 제거
사용자 노출 문자열은 i18n 키를 사용하고, path는 의존성에서 제거해 재오픈을 방지하세요.

-  useEffect(() => {
-    isModalOpen &&
-      Swal.fire({
-        title: 'Redirecting',
-        text: 'Do you want to get group information?',
-        icon: 'info',
-        showCancelButton: true,
-      }).then((result) => {
-        if (result.isConfirmed) {
-          thirdPartyAuth();
-          setIsModalOpen(false);
-        }
-        setIsModalOpen(false);
-      });
-  }, [path, isModalOpen]);
+  useEffect(() => {
+    if (!isModalOpen) return;
+    Swal.fire({
+      title: t('common.confirm'),
+      text: t('group.loginGroupsClient'),
+      icon: 'info',
+      showCancelButton: true,
+    }).then((result) => {
+      if (result.isConfirmed) {
+        thirdPartyAuth();
+      }
+      setIsModalOpen(false);
+    });
+  }, [isModalOpen, t]);

97-107: 변수명 섀도잉 해소 + 로그 정리
group(prop)과 지역 변수명이 겹칩니다. 의미를 드러내도록 변경하고, 콘솔 로그는 i18n 토스트 등으로 대체 고려.

-      const group = await getUserInfo(accessToken);
-      if (!group) {
-        console.log(`User do not belong to any group`);
+      const userGroups = await getUserInfo(accessToken);
+      if (!userGroups) {
+        // TODO: 사용자에게 안내 노출
         //TODO: handle if user do not belong to any group
         return;
       }
-      setMyGroups(group);
+      setMyGroups(userGroups);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e524100 and b35da10.

📒 Files selected for processing (7)
  • src/api/group/group.ts (2 hunks)
  • src/app/[lng]/page.tsx (1 hunks)
  • src/app/[lng]/thirdParty.tsx (1 hunks)
  • src/app/[lng]/write/SelectAccountArea.tsx (1 hunks)
  • src/app/components/shared/Zabo/Zabo.tsx (2 hunks)
  • src/app/i18next/locales/en/translation.json (1 hunks)
  • src/app/i18next/locales/ko/translation.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/[lng]/page.tsx
  • src/app/components/shared/Zabo/Zabo.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-11-18T11:59:04.320Z
Learnt from: dohyun-ko
PR: gsainfoteam/ziggle-fe#388
File: src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ClientActions.tsx:37-43
Timestamp: 2024-11-18T11:59:04.320Z
Learning: `ClientActions.tsx` 파일의 `handleWithdrawal` 함수에서 외부 URL을 열 때, 사용자에게 확인을 받지 않아도 됩니다.

Applied to files:

  • src/app/[lng]/thirdParty.tsx
📚 Learning: 2025-09-15T12:56:30.285Z
Learnt from: SHIM-JINSEO
PR: gsainfoteam/ziggle-fe#439
File: src/api/group/group.ts:24-27
Timestamp: 2025-09-15T12:56:30.285Z
Learning: In the ziggle-fe project, URL environment variables are standardized to always include trailing slashes, making URL concatenation safe without additional normalization.

Applied to files:

  • src/api/group/group.ts
🧬 Code graph analysis (1)
src/app/[lng]/write/SelectAccountArea.tsx (2)
src/app/i18next/index.ts (1)
  • PropsWithT (65-65)
src/api/group/group.ts (4)
  • UserInfo (32-32)
  • thirdPartyAuth (68-72)
  • getGroupsToken (74-86)
  • getUserInfo (88-100)
🔇 Additional comments (2)
src/api/group/group.ts (1)

18-20: 권한 타입 과도 제한 가능성(고정 튜플)
permissions: ['MEMBER_UPDATE']는 단일 값으로 고정됩니다. API가 가변 권한을 돌려준다면 string[]로 넓혀야 합니다. 실제 응답 형태 확인 부탁드립니다.

-      permissions: ['MEMBER_UPDATE'];
+      permissions: string[];
src/app/[lng]/write/SelectAccountArea.tsx (1)

53-57: 프로필 이미지: key ↔ url 혼용 가능성
profileImageUrlprofileImageKey를 그대로 넣고 있습니다. API가 실제 URL을 주는지 확인 필요합니다. 키만 온다면 URL 변환 헬퍼를 거치거나 타입을 profileImageKey로 맞추세요.

-      const groupData = {
+      const groupData = {
         uuid: selectedGroup?.uuid,
         name: selectedGroup?.name,
-        profileImageUrl: selectedGroup?.profileImageKey || null,
+        // 확인 필요: API가 URL을 내려주면 아래처럼 교체
+        // profileImageUrl: selectedGroup?.profileImageUrl ?? null,
+        // 키만 온다면 변환 함수 사용 예:
+        // profileImageUrl: selectedGroup?.profileImageKey ? getImageUrlFromKey(selectedGroup.profileImageKey) : null,
       };

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Get Group containing me

2 participants