Skip to content

feat: 대학 정보 멀티존 분리#543

Open
manNomi wants to merge 9 commits into
mainfrom
feat/university-ssg-multizone
Open

feat: 대학 정보 멀티존 분리#543
manNomi wants to merge 9 commits into
mainfrom
feat/university-ssg-multizone

Conversation

@manNomi
Copy link
Copy Markdown
Contributor

@manNomi manNomi commented Jun 2, 2026

관련 이슈

작업 내용

  • 대학 정보 catalog SSG 페이지를 apps/university-web 별도 Next.js zone으로 분리했습니다.
  • apps/web에는 /university/score/*, /university/application/*만 남기고, catalog route는 UNIVERSITY_WEB_DOMAIN 기반 rewrite로 연결했습니다.
  • apps/university-webassetPrefix: "/university-static", SSG route revalidate = false, dynamicParams = false를 적용했습니다.
  • SSG 데이터 fetch 실패 시 빈 catalog가 정적으로 굳지 않도록 build failure로 처리했습니다.
  • zone 간 이동에서 catalog 경계는 hard navigation으로 처리해 Next prefetch가 없는 route를 찾지 않게 했습니다.
  • CI와 husky hook에 university-web 검증/빌드 경로를 추가했습니다.
  • Vercel 멀티존 배포 체크리스트 문서를 추가했습니다.
  • apps/web에서 대학 정보 catalog 이동은 <a>를 사용하도록 lint 규칙을 추가했습니다.

빌드타임 개선 측정

비교 기준

  • 이슈 대학 상세 페이지 정적 생성으로 인한 웹 빌드 시간 개선 #542 기준 최근 GitHub Actions Web - Build job은 약 40 ~ 48초, 실제 Build web application 단계는 약 25~27초였습니다.
  • 기존 web build 로그에서는 Generating static pages using 3 workers (188/188) in 5.6s로, 대학 상세 SSG가 web build 안에서 함께 수행됐습니다.
  • 이번 PR 측정은 GitHub Actions run 26805266005Web - Build job 79021184400 기준입니다.

개선 결과

항목 변경 전 변경 후 효과
main web 정적 생성 페이지 수 188개 35개 153개 감소, 약 81% 감소
main web static generation 시간 5.6초 1.626초 3.974초 감소, 약 71% 감소
main web Build web application 단계 약 25~27초 로그 타임스탬프 기준 약 23초 약 2~4초 감소
전체 Web - Build job 약 40~48초 44초 setup/install 등 고정 비용 때문에 기존 범위 내

해석

  • 이슈에서 지적한 병목인 “web build 안에서 대학 상세 SSG까지 전량 생성하는 구조”는 제거됐습니다.
  • main web build에는 이제 /university/score/*, /university/application/*만 남고, 대학 정보 catalog SSG는 apps/university-web에서 별도로 생성됩니다.
  • PR CI 기준 apps/university-webGenerating static pages using 3 workers (158/158) in 3.3s로 catalog SSG를 유지합니다. 즉, SEO/GEO를 위한 정적 페이지 생성은 유지하되 main web 배포와 분리했습니다.
  • 전체 CI에서 web과 university-web을 모두 빌드하면 job 수는 늘지만, Vercel 멀티존 배포에서는 변경 범위에 따라 배포 단위를 분리할 수 있습니다. 따라서 대학 데이터/대학 정보 버그 수정은 main web 전체를 다시 빌드하지 않고 university-web 쪽 배포로 처리할 수 있습니다.

특이 사항

  • production web build에는 UNIVERSITY_WEB_DOMAIN이 필수입니다. 누락 시 /university catalog가 404가 되므로 next.config.mjs에서 의도적으로 build fail 처리했습니다.
  • Vercel에서는 apps/university-web를 별도 Project로 만들고 Root Directory를 apps/university-web로 설정해야 합니다.
  • legacy /university/list/:homeUniversity는 child route를 유지하지 않고 /university/:homeUniversity SSG 페이지로 rewrite합니다.
  • 직접 university-web 배포 도메인은 robots.txt에서 Disallow: /로 차단합니다.

검증

  • pnpm install --frozen-lockfile
  • pnpm --filter @solid-connect/web run lint:check
  • pnpm --filter @solid-connect/university-web run lint:check
  • pnpm --filter @solid-connect/web run typecheck
  • pnpm --filter @solid-connect/university-web run typecheck
  • UNIVERSITY_WEB_DOMAIN=https://solid-connect-university-web.vercel.app pnpm --filter @solid-connect/web run build
  • pnpm --filter @solid-connect/university-web run build
  • env -u UNIVERSITY_WEB_DOMAIN pnpm --filter @solid-connect/web run build 실패 확인
  • node scripts/check-university-zone-navigation.mjs
  • pnpm exec biome check scripts/check-university-zone-navigation.mjs
  • commit hook CI parity checks 통과
  • push hook CI parity checks + web/university-web/admin build 통과
  • 독립 코드리뷰 루프 2회 진행, 2차 Critical/High 없음

리뷰 요구사항 (선택)

  • apps/web/next.config.mjs의 rewrite 범위가 의도한 catalog route만 잡는지 확인 부탁드립니다.
  • Vercel Project/Root Directory/env 설정 문서가 실제 운영 설정에 충분한지 확인 부탁드립니다.

@manNomi manNomi requested review from enunsnv and wibaek as code owners June 2, 2026 07:19
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

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

Project Deployment Actions Updated (UTC)
solid-connect-university-web Ready Ready Preview, Comment Jun 3, 2026 7:18am
solid-connect-web-web Error Error Jun 3, 2026 7:18am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
solid-connect-web-admin Skipped Skipped Jun 3, 2026 7:18am
solid-connection-web Skipped Skipped Jun 3, 2026 7:18am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7463de38-c068-4fad-b680-eaec574079a0

📥 Commits

Reviewing files that changed from the base of the PR and between eae28e7 and 6f9376c.

📒 Files selected for processing (5)
  • .github/workflows/release.yml
  • apps/university-web/.env
  • apps/university-web/.env.development
  • apps/university-web/.env.production
  • apps/university-web/sentry.server.config.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/university-web/.env.production
  • apps/university-web/.env.development
  • .github/workflows/release.yml
  • apps/university-web/.env

Walkthrough

  1. CI/릴리즈
    • detect-changes 출력에 university-web 추가, paths-filter에 apps/university-web/** 추가
    • university-web-quality-check 및 university-web-build 잡 추가 (UNIVERSITY_WEB_DOMAIN, NODE_ENV 주입)
    • pre-commit / pre-push 훅에 RUN_UNIVERSITY_WEB 플래그와 분기 추가
    • release.yml에 target 옵션으로 university 및 all 추가, release-university 분기 추가
  2. 앱 구성
    • apps/university-web 패키지 추가: package.json, next.config.mjs, postcss.config.js, biome.json, components.json, headver.json, images.d.ts, instrumentation.ts
    • Sentry 설정 파일(sentry.server.config.ts, sentry.edge.config.ts) 및 register 계측 추가
    • .env, .env.development, .env.production 환경변수 파일 추가
    • 정적 에셋: public/naver*.html 및 SVG 배럴 인덱스 다수 추가
    • 문서 추가: AUTHENTICATION.md, COMPONENTS.md, REACT_COMPILER.md
  3. API/훅
    • 공통: QueryKeys 정의 추가
    • Admin, Auth, MyPage, Scores, Applications, Chat, Community, Image-upload, Kakao-api, Mentor, News, Reports, Universities 도메인별 API 클라이언트와 타입 추가
    • 각 도메인에 대해 React Query 훅(useQuery/useMutation/useInfiniteQuery)과 바렐(index.ts) 추가

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • wibaek
  • enunsnv
✨ 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/university-ssg-multizone

@manNomi
Copy link
Copy Markdown
Contributor Author

manNomi commented Jun 2, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3f301ab1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

const universityData = universityDetailResult.data;
const universityData = await getUniversityDetailForSsg(collegeId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the detail fallback on non-OK API responses

When a preview ID from generateStaticParams() has a detail endpoint that returns any non-OK response, getUniversityDetailForSsg() throws via assertUniversitySsgResponse; the previous page used getUniversityDetailWithStatus() and rendered UniversityDetailPreparingFallback for non-404 failures. In that scenario, one preparing or temporarily failing university detail now breaks the university-web static build or produces an error page instead of the intended fallback, so this should keep the status-aware fetch path before rendering detail content.

Useful? React with 👍 / 👎.

};

const UniversityListContent = (props: UniversityListContentProps) => (
<Suspense fallback={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.

P2 Badge Avoid null prerender for the catalog list

For these statically generated /university/:homeUniversity pages, useSearchParams() inside the child causes the subtree up to this Suspense boundary to be client-rendered; because the fallback is null, the prerendered HTML contains no search bar, count, or university cards until hydration. The old implementation parsed searchParams in the server page and still emitted the list in the initial HTML, so crawlers/no-JS users and slow clients now see a blank catalog body; pass the initial query values from the page or use a meaningful server-rendered fallback instead of suspending the whole list to null.

Useful? React with 👍 / 👎.

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: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (16)
apps/university-web/src/apis/chat/normalize.ts-62-92 (1)

62-92: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

서버 id가 빠진 동일 메시지끼리 폴백 id가 겹칠 수 있어요.

getFallbackMessageIdsenderId|createdAt|content|attachmentSignature만으로 해시를 만들기 때문에, 같은 사용자가 같은 초에 동일한 내용을 두 번 보내면 완전히 다른 메시지인데도 같은 음수 id가 생성됩니다. 이렇게 되면 리스트 렌더링 시 React key가 충돌해 메시지가 섞이거나 누락되어 보일 수 있습니다.

normalizeChatMessage를 호출하는 쪽(.map(normalizeChatMessage))에서 이미 배열 인덱스를 받을 수 있으니, 인덱스를 시드에 함께 섞어주면 적은 비용으로 충돌을 피할 수 있습니다.

🧩 인덱스를 시드에 포함하는 제안
-const getFallbackMessageId = (message: RawChatMessage): number => {
+const getFallbackMessageId = (message: RawChatMessage, index = 0): number => {
   const senderId = toNumber(message.senderId ?? message.siteUserId);
   const attachmentSignature = (message.attachments ?? [])
     .map((attachment) => `${attachment.isImage ? "image" : "file"}:${attachment.url}:${attachment.createdAt}`)
     .join(",");
-  const seed = `${senderId}|${message.createdAt}|${message.content}|${attachmentSignature}`;
+  const seed = `${index}|${senderId}|${message.createdAt}|${message.content}|${attachmentSignature}`;

   // 서버에서 id가 누락되는 경우를 대비해 항상 동일한 임시 음수 id를 생성합니다.
   return -createStableHash(seed);
 };
@@
-export const normalizeChatMessage = (message: RawChatMessage): ChatMessage => {
+export const normalizeChatMessage = (message: RawChatMessage, index = 0): ChatMessage => {
   const parsedId = toNumber(message.id);
-  const normalizedId = parsedId > 0 ? parsedId : getFallbackMessageId(message);
+  const normalizedId = parsedId > 0 ? parsedId : getFallbackMessageId(message, index);
🤖 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 `@apps/university-web/src/apis/chat/normalize.ts` around lines 62 - 92,
getFallbackMessageId can produce identical negative ids for distinct messages
sent by the same user at the same timestamp; fix by including the message array
index in the seed. Change getFallbackMessageId to accept an additional index
parameter (e.g. getFallbackMessageId(message, index)) and include that index in
the seed string, update normalizeChatMessage signature to accept an optional
index (normalizeChatMessage(message, index)) and when mapping messages call it
with the map index (e.g. messages.map((m, i) => normalizeChatMessage(m, i))).
This keeps fallback ids stable but unique per position and only affects cases
where parsed id <= 0.
apps/university-web/src/apis/Auth/postEmailVerification.ts-9-13 (1)

9-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

파일명과 실제 동작이 어긋나 혼동을 줍니다.

파일명은 postEmailVerification.ts인데, 내용은 usePostEmailSignUpauthApi.postEmailSignUp(이메일 회원가입)을 호출합니다. 검증(verification)과 회원가입(sign-up)은 별개 흐름이므로, 파일/훅 이름을 동작에 맞춰 정리하면 유지보수 시 오해를 막을 수 있습니다.

🤖 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 `@apps/university-web/src/apis/Auth/postEmailVerification.ts` around lines 9 -
13, The file name and implementation disagree: postEmailVerification.ts contains
a hook named usePostEmailSignUp that calls authApi.postEmailSignUp; choose one
consistent behavior and update names/calls accordingly — either rename the file
to postEmailSignUp.ts (or similar) to match usePostEmailSignUp and keep calling
authApi.postEmailSignUp, or change the hook to usePostEmailVerification and its
mutationFn to call authApi.postEmailVerification so the file, hook name, and API
call all reflect the verification flow; update any imports/usages to the new
name to avoid breaking references.
apps/university-web/src/apis/Auth/deleteAccount.ts-17-28 (1)

17-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

탈퇴 요청 실패 시 사용자가 로그인 상태로 홈에 남는 어긋남이 있습니다.

onMutate에서 먼저 router.replace("/")로 이동하지만, 토큰 정리(clearAccessToken)와 캐시 비우기는 onSuccess에서만 수행됩니다. 그래서 deleteAccount가 실패하면 사용자는 이미 홈으로 이동했음에도 토큰이 남아 여전히 로그인 상태가 되어, "탈퇴된 줄 알았는데 아니다"라는 혼란을 줄 수 있습니다. onError로 실패를 안내하거나, 낙관적 이동 대신 onSuccess에서 이동하도록 정리하는 편이 안전합니다.

🛟 제안 변경
   return useMutation<AccountResponse, AxiosError, void>({
     mutationFn: () => authApi.deleteAccount(),
-    onMutate: () => {
-      // 낙관적 업데이트: 요청이 시작되면 바로 홈으로 이동
-      router.replace("/");
-    },
     onSuccess: () => {
       // Zustand persist가 자동으로 localStorage에서 제거
       clearAccessToken();
       queryClient.clear();
+      router.replace("/");
+    },
+    onError: () => {
+      // 실패 시 사용자에게 안내 (로그인 상태 유지)
+      showIconToast("error", "회원탈퇴에 실패했습니다. 다시 시도해 주세요.");
     },
   });
🤖 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 `@apps/university-web/src/apis/Auth/deleteAccount.ts` around lines 17 - 28, The
current optimistic navigation in useMutation (onMutate -> router.replace("/"))
moves the user before authApi.deleteAccount completes, while clearAccessToken()
and queryClient.clear() run only in onSuccess, causing the user to remain logged
if the request fails; update the mutation to perform router.replace("/"),
clearAccessToken(), and queryClient.clear() inside onSuccess instead of
onMutate, and add an onError handler that shows an error (or reverts navigation)
so failures keep the user logged in and informed (refer to useMutation's
onMutate, onSuccess, onError, authApi.deleteAccount, clearAccessToken,
router.replace, and queryClient.clear).
apps/university-web/src/apis/Auth/api.ts-44-51 (1)

44-51: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

EmailVerification 타입이 이메일 “검증” 흐름에 연결되지 않는 상태입니다.

  1. apps/university-web/src/apis/Auth/api.tsEmailVerificationRequest/Response( verificationCode 포함 )만 있고, authApi 안에 이를 쓰는 인증코드 검증 메서드(postEmailVerification 등)는 보이지 않습니다.
  2. apps/university-web/src/apis/Auth/postEmailVerification.tsusePostEmailSignUp 훅으로 동작하며 authApi.postEmailSignUp만 호출해 EmailVerification* 타입을 전혀 사용하지 않습니다. (파일명/훅명도 postEmailVerification vs usePostEmailSignUp로 어긋나 있어요.)
  3. apps/university-web/src/apis/queryKeys.tsemailVerification queryKey도 실제 호출부가 확인되지 않습니다.
🤖 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 `@apps/university-web/src/apis/Auth/api.ts` around lines 44 - 51, The
EmailVerification types are declared but never wired into the API surface or
hooks; add a dedicated method authApi.postEmailVerification that accepts
EmailVerificationRequest and returns EmailVerificationResponse, update
apps/university-web/src/apis/Auth/postEmailVerification.ts to call
authApi.postEmailVerification (not authApi.postEmailSignUp), rename the hook to
usePostEmailVerification (or align the file/hook name) so it uses the
verificationCode, and ensure queryKeys.emailVerification is referenced where the
verification call is made; adjust imports and tests accordingly to use
EmailVerificationRequest/Response and the new authApi method.
apps/university-web/src/apis/Auth/postSignOut.ts-14-23 (1)

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

서버 로그아웃이 실패하면 로컬 세션이 그대로 남습니다.

정리 로직이 onSuccess에만 있어서, 네트워크 오류나 서버 5xx로 postSignOut()이 실패하면 clearAccessToken()queryClient.clear()가 호출되지 않아 사용자가 로그아웃하지 못한 채 갇히게 됩니다. 로그아웃은 로컬 정리가 우선되어야 하므로, 성공/실패와 무관하게 정리되도록 onSettled로 옮기는 것을 권합니다.

🛡️ 제안
   return useMutation<SignOutResponse, AxiosError, void>({
     mutationFn: () => authApi.postSignOut(),
-    onSuccess: () => {
-      // Zustand persist가 자동으로 localStorage에서 제거
-      clearAccessToken();
-      queryClient.clear();
-      // 로그아웃 후 홈으로 리다이렉트
-      window.location.href = "/";
-    },
+    onSettled: () => {
+      // 서버 응답과 무관하게 로컬 인증 상태를 정리
+      clearAccessToken();
+      queryClient.clear();
+      window.location.href = "/";
+    },
   });
🤖 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 `@apps/university-web/src/apis/Auth/postSignOut.ts` around lines 14 - 23, Move
the local cleanup from the mutation's onSuccess to onSettled so cleanup runs
regardless of server response: in the useMutation call that invokes
authApi.postSignOut() (see mutationFn/postSignOut), remove clearAccessToken()
and queryClient.clear() from onSuccess and place them into an onSettled handler
that always calls clearAccessToken() and queryClient.clear(), while keeping the
redirect to "/" in onSuccess if you still want to navigate only on successful
server sign-out.
apps/university-web/src/apis/Auth/index.ts-16-25 (1)

16-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

usePostRefreshToken 배럴 누락 + 앱 내 사용 흔적 부재 확인 필요

  1. 배럴 재-export 누락
    • apps/university-web/src/apis/Auth/index.tsusePostRefreshToken이 재-export되어 있지 않습니다.
  2. 실사용 흔적 없음(죽은 코드 가능성 큼)
    • apps/university-web/src에서 usePostRefreshTokenapps/university-web/src/apis/Auth/postRefreshToken.ts 내부 선언/내보내기 라인에서만 확인되고, 해당 훅을 가져다 쓰는 import/사용처가 보이지 않습니다.
  3. 결정/정리 제안
    • 실제로 쓰려던 훅이면 index.ts에 재-export 추가(또는 실제 호출자가 있다면 import 경로 확인)로 접근성을 맞추고,
    • 안 쓰는 훅이면 postRefreshToken.ts 정리(삭제/제거)로 죽은 코드 리스크를 줄이세요.
🤖 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 `@apps/university-web/src/apis/Auth/index.ts` around lines 16 - 25, The barrel
file is missing the re-export for the hook usePostRefreshToken declared in
postRefreshToken.ts and there are no usages in the codebase (possible dead
code); either add a re-export for usePostRefreshToken in index.ts alongside the
other exports so callers can import it, or if the hook is unused, remove/clean
up postRefreshToken.ts to eliminate dead code—search for usePostRefreshToken and
postRefreshToken.ts and then either add the export to index.ts or delete the
unused file accordingly.
apps/university-web/src/apis/kakao-api/api.ts-3-9 (1)

3-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Kakao 응답 타입을 undefined로 두면 data 필드 접근이 타입에서 막힙니다 🧩

apps/university-web/src/apis/kakao-api/api.ts에서 KakaoUserIdsResponse/KakaoUnlinkResponse/KakaoInfoResponse가 전부 undefined로 선언돼 있어 res.data와 React Query 훅의 data 타입이 전부 undefined로 추론됩니다.
그 결과 getKakaoInfo에서 kakao_account.email 같은 값을 캐스팅/타입 무시 없이 읽기 어렵습니다.

export type KakaoUserIdsResponse = undefined;

export type KakaoUnlinkResponse = undefined;

export type KakaoUnlinkRequest = Record<string, never>;

export type KakaoInfoResponse = undefined;
  • v2/user/mekakao_account.email을 포함하는 형태이므로 문서 JSON 스키마대로 KakaoInfoResponse 타입을 구체화해 주세요.
  • v1/user/idselements(ID 배열) 중심으로 타입을 잡아 주세요.
  • v1/user/unlink는 성공/실패 바디 포맷이 문서 예시 기준으로 달라질 수 있으니(특히 성공 응답 바디) 카카오 문서의 응답 예시를 확인한 뒤 KakaoUnlinkResponse를 확정해 주세요.
🤖 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 `@apps/university-web/src/apis/kakao-api/api.ts` around lines 3 - 9, The
current response types (KakaoUserIdsResponse, KakaoUnlinkResponse,
KakaoInfoResponse) are all declared as undefined which prevents accessing
res.data and breaks getKakaoInfo; update these to concrete interfaces that match
Kakao docs: define KakaoInfoResponse to include the top-level id and
kakao_account with email (and other optional fields) to allow reading
kakao_account.email in getKakaoInfo, define KakaoUserIdsResponse to include
elements: string[] (and any pagination/meta fields per v1/user/ids), and set
KakaoUnlinkResponse to the documented v1/user/unlink success/error shape (or a
union if it varies); keep KakaoUnlinkRequest as Record<string, never> if
correct, and then update any usages (e.g., getKakaoInfo and React Query hooks)
to use these new types so res.data is properly typed.
apps/university-web/src/apis/MyPage/getProfile.ts-24-34 (1)

24-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

낙관적 병합이 요청 객체를 통째로 펼치고 있어요.

select가 변이 변수를 Partial<MyInfoResponse>로 캐스팅하지만, 실제 변수 타입은 ProfilePatchRequest(nickname?, file?)예요. 그래서 { ...queryResult.data, ...pendingData }로 병합하면 사용자 정보에 File 객체인 file 속성이 그대로 끼어들고, 정작 표시용 프로필 이미지(URL)는 낙관적으로 갱신되지 않습니다.

또한 호출부가 nickname: undefined를 포함해 넘기면 기존 닉네임이 undefined로 덮여 화면에서 잠깐 비어 보일 수 있어요. 병합 대상을 닉네임처럼 실제로 표시 가능한 필드로 좁혀 두는 편이 안전합니다.

🛠️ 병합 필드를 좁히는 제안
-  const pendingMutations = useMutationState({
-    filters: {
-      mutationKey: [QueryKeys.MyPage.profile, "patch"],
-      status: "pending",
-    },
-    select: (mutation) => {
-      return mutation.state.variables as Partial<MyInfoResponse>;
-    },
-  });
-
-  const isOptimistic = pendingMutations.length > 0;
-  const pendingData = isOptimistic ? pendingMutations[0] : null;
-
-  const displayData = isOptimistic && queryResult.data ? { ...queryResult.data, ...pendingData } : queryResult.data;
+  const pendingMutations = useMutationState({
+    filters: {
+      mutationKey: [QueryKeys.MyPage.profile, "patch"],
+      status: "pending",
+    },
+    select: (mutation) => mutation.state.variables as ProfilePatchRequest,
+  });
+
+  const pendingData = pendingMutations.at(0);
+  const displayData =
+    pendingData && queryResult.data
+      ? { ...queryResult.data, ...(pendingData.nickname ? { nickname: pendingData.nickname } : {}) }
+      : queryResult.data;
🤖 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 `@apps/university-web/src/apis/MyPage/getProfile.ts` around lines 24 - 34, The
optimistic merge is spreading the mutation variables (ProfilePatchRequest) into
MyInfoResponse, which can inject a File under `file` and overwrite displayable
fields with undefined; modify the merge so it only applies explicit displayable
fields (e.g., `nickname`, and `profileImageUrl` derived after file upload) from
the pending mutation: in the `select`/merge logic (look at `select`,
`pendingMutations`, `displayData`, and `queryResult.data`) pick or map only
allowed keys, ignore `file` directly, and filter out undefined values so
existing profile fields are not overwritten by undefined.
apps/university-web/src/apis/news/deleteNews.ts-6-39 (1)

6-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1. 캐시 타입과 컨텍스트 타입이 실제 저장 형태와 어긋나 있어요.

이 쿼리 키에는 setQueryData<ArticleListResponse>(...)ArticleListResponse 객체가 저장되는데, 스냅샷은 getQueryData<Article[]>로 읽고 컨텍스트도 Article[]로 선언되어 있습니다. 런타임에서는 저장된 객체가 그대로 복원되어 롤백 자체는 동작하지만, 타입 정보가 실제와 달라서 이후 previousArticleList를 다룰 때 잘못된 형태를 가정하기 쉬워집니다. ArticleListResponse로 통일해 두면 타입과 실제 데이터가 일치해 안전해집니다.

🔧 제안: 타입 일치 및 미사용 import 정리
-import type { Article } from "`@/types/news`";
 import { type ArticleListResponse, NewsQueryKeys, newsApi } from "./api";

 type ArticleDeleteMutationContext = {
-  previousArticleList?: Article[];
+  previousArticleList?: ArticleListResponse;
 };
-      const previousArticleList = queryClient.getQueryData<Article[]>(queryKey);
+      const previousArticleList = queryClient.getQueryData<ArticleListResponse>(queryKey);
     onError: (_error, _variables, context) => {
       if (context?.previousArticleList) {
-        queryClient.setQueryData<Article[]>(queryKey, context.previousArticleList);
+        queryClient.setQueryData<ArticleListResponse>(queryKey, context.previousArticleList);
       }
     },
🤖 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 `@apps/university-web/src/apis/news/deleteNews.ts` around lines 6 - 39, The
context and cache types are inconsistent: you store an ArticleListResponse via
setQueryData but read and snapshot as Article[]; update the context type
ArticleDeleteMutationContext to hold previousArticleList?: ArticleListResponse,
change the getQueryData call to getQueryData<ArticleListResponse>(queryKey) and
ensure the returned snapshot and onError restore use ArticleListResponse (not
Article[]), and adjust any setQueryData/getQueryData generic parameters in
useDeleteArticle (mutationFn/newsApi.deleteArticle, queryKey, onMutate, onError)
so types match the actual stored shape; also remove any now-unused imports
related to Article[] if present.
apps/university-web/src/apis/news/putUpdateNews.ts-6-48 (1)

6-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1. 스냅샷/컨텍스트 타입을 ArticleListResponse로 맞춰 주세요.

deleteNews.ts, postLikeNews.ts와 동일한 패턴으로, 캐시에는 ArticleListResponse가 저장되지만 여기서는 Article[]로 읽고 선언하고 있어요. 롤백은 같은 객체를 되돌려서 동작하지만, 타입과 실제 데이터가 일치하도록 정리하면 이후 유지보수가 더 안전합니다.

🔧 제안: 타입 일치 및 미사용 import 정리
-import type { Article } from "`@/types/news`";
 import { type ArticleListResponse, NewsQueryKeys, newsApi, type UsePutModifyArticleRequest } from "./api";

 type ArticleMutationContext = {
-  previousArticleList?: Article[];
+  previousArticleList?: ArticleListResponse;
 };
-      const previousArticleList = queryClient.getQueryData<Article[]>(queryKey);
+      const previousArticleList = queryClient.getQueryData<ArticleListResponse>(queryKey);
🤖 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 `@apps/university-web/src/apis/news/putUpdateNews.ts` around lines 6 - 48,
Change the snapshot/context type to ArticleListResponse throughout: update the
ArticleMutationContext so previousArticleList is ArticleListResponse |
undefined, adjust the usePutModifyArticle onMutate to call
queryClient.getQueryData<ArticleListResponse>(queryKey) (not Article[]), return
that previousArticleList, and ensure onError sets the query data with that
ArticleListResponse value; also update any related generics/usages of
setQueryData/getQueryData in usePutModifyArticle to ArticleListResponse and
remove any now-unused imports.
apps/university-web/src/apis/news/postLikeNews.ts-7-49 (1)

7-49: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1. 컨텍스트/스냅샷 타입이 실제 캐시 형태와 다릅니다.

deleteNews.ts와 동일하게, 이 쿼리 키에는 ArticleListResponse가 저장되는데 스냅샷은 Article[]로 읽고 컨텍스트도 Article[]로 선언되어 있어요. 런타임 롤백은 동작하지만 타입이 실제와 어긋나니 ArticleListResponse로 맞추는 편이 안전합니다.

2. 좋아요 수치 정산 단계가 없습니다.

deleteLikeNews.ts와 마찬가지로 onSettled 재조회가 없어 서버 likeCount와 표시값이 어긋날 수 있으니 함께 보강하면 좋습니다.

🔧 제안: 타입 일치 + 정산 단계
-import type { Article } from "`@/types/news`";
 import { type ArticleListResponse, NewsQueryKeys, newsApi, type PostArticleLikeResponse } from "./api";

 type ArticleLikeMutationContext = {
-  previousArticleList?: Article[];
+  previousArticleList?: ArticleListResponse;
 };
-      const previousArticleList = queryClient.getQueryData<Article[]>(queryKey);
+      const previousArticleList = queryClient.getQueryData<ArticleListResponse>(queryKey);
     onError: (_err, _variables, context) => {
       if (context?.previousArticleList) {
-        queryClient.setQueryData<Article[]>(queryKey, context.previousArticleList);
+        queryClient.setQueryData<ArticleListResponse>(queryKey, context.previousArticleList);
       }
     },
+
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey });
+    },
   });
🤖 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 `@apps/university-web/src/apis/news/postLikeNews.ts` around lines 7 - 49, The
mutation context and snapshot should match the cached shape and you should add a
post-settlement refresh: change ArticleLikeMutationContext (and any uses in
onMutate/onError) so previousArticleList is an ArticleListResponse (not
Article[]), update the get/setQueryData calls in usePostArticleLike (function
name) to read/write ArticleListResponse consistently for queryKey, and add an
onSettled handler that refetches/invalidate the queryKey (e.g.,
queryClient.invalidateQueries or refetchQueries) to reconcile server likeCount
after the optimistic update.
apps/university-web/COMPONENTS.md-399-399 (1)

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

1) 리소스 링크가 플레이스홀더 상태입니다.

- Line 399의 `link-to-figma`는 실제 이동이 불가합니다. 온보딩 문서 신뢰도를 위해 실 URL로 교체하거나, 미정이면 TODO 표기로 의도를 명확히 해 주세요.
🤖 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 `@apps/university-web/COMPONENTS.md` at line 399, Replace the placeholder
markdown link "[Figma Design System](link-to-figma)" with the actual Figma URL
for the project's design system or, if the URL is not yet available, change the
link target to a clear TODO marker (e.g., "[Figma Design System](TODO: add figma
URL)") and optionally add a short comment note beside "Figma Design System"
indicating who should supply the link; update the entry so it is not a dead
placeholder and is clearly actionable.
apps/university-web/REACT_COMPILER.md-23-24 (1)

23-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1) 검증 명령의 패키지 타깃이 현재 앱과 다릅니다.

- Line 23~24는 `@solid-connect/web`를 검사하고 있어 `apps/university-web` 롤아웃 검증과 불일치합니다. 문서 목적에 맞게 필터 대상을 university-web로 맞춰 주세요.
🔧 제안 수정안
-- Run `pnpm --filter `@solid-connect/web` build` and confirm the compiled server/client output references `react/compiler-runtime`.
-- Run `pnpm --filter `@solid-connect/web` ci:check` before merging.
+- Run `pnpm --filter `@solid-connect/university-web` build` and confirm the compiled server/client output references `react/compiler-runtime`.
+- Run `pnpm --filter `@solid-connect/university-web` ci:check` before merging.
🤖 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 `@apps/university-web/REACT_COMPILER.md` around lines 23 - 24, The two
verification commands in REACT_COMPILER.md target the wrong package
(`@solid-connect/web`); update both commands so the --filter target matches the
current app (use university-web or the package name used in your repo, e.g.,
--filter university-web) so that the build and ci:check steps run against the
apps/university-web project (change the lines that run `pnpm --filter
`@solid-connect/web` build` and `pnpm --filter `@solid-connect/web` ci:check` to use
the correct filter).
apps/university-web/AUTHENTICATION.md-34-60 (1)

34-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1) 문서의 대상 앱 경로가 현재 코호트와 어긋나 있습니다.

- Line 34~60에서 `apps/web/...` 경로를 기준으로 설명하고 있어, `apps/university-web` 문서로 읽을 때 책임 경계와 추적 경로가 혼동됩니다. 같은 문서 내 경로 기준을 university-web로 맞춰 주세요.
🔧 제안 수정안(예시)
-`apps/web/src/middleware.ts`는 현재 아래만 담당합니다.
+`apps/university-web/src/middleware.ts`는 현재 아래만 담당합니다.

-- `apps/web/src/lib/zustand/useAuthStore.ts`
-- `apps/web/src/utils/axiosInstance.ts`
-- `apps/web/src/components/layout/ReissueProvider/index.tsx`
-- `apps/web/src/middleware.ts`
+- `apps/university-web/src/lib/zustand/useAuthStore.ts`
+- `apps/university-web/src/utils/axiosInstance.ts`
+- `apps/university-web/src/components/layout/ReissueProvider/index.tsx`
+- `apps/university-web/src/middleware.ts`
🤖 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 `@apps/university-web/AUTHENTICATION.md` around lines 34 - 60, The doc uses
paths like `apps/web/...` which don't match this repo's app name
(`university-web`), causing confusion; update all path references in
AUTHENTICATION.md (e.g., entries under "Related Files" and anywhere
`apps/web/src/...` is mentioned, including `apps/web/src/middleware.ts`,
`apps/web/src/lib/zustand/useAuthStore.ts`,
`apps/web/src/utils/axiosInstance.ts`,
`apps/web/src/components/layout/ReissueProvider/index.tsx`) to the correct
`apps/university-web/...` equivalents (or make them relative like `src/...` if
that better reflects the project layout) so responsibility and traceability
align with the current codebase.
apps/university-web/next.config.mjs-63-65 (1)

63-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

typescript.ignoreBuildErrors: true를 유지할 근거(또는 보강) 필요

  1. CI에서는 university-web-quality-checkpnpm --filter @solid-connect/university-web run ci:checktypecheck:ci(tsc --noEmit -p tsconfig.ci.json)를 먼저 통과해야 university-web-build가 실행되므로, 해당 워크플로우 기준으로 “타입 계약이 깨진 채 배포된다”는 우려는 과장입니다.
    • 다만 ignoreBuildErrors: truenext build 단계에서 타입 오류를 실패로 만들지 않을 수 있으니(로컬/수동/다른 파이프라인 포함), ignoreBuildErrorsfalse로 두거나 이 설정을 둔 이유를 명시하는 쪽이 더 안전합니다.
수정 예시
   typescript: {
-    ignoreBuildErrors: true,
+    ignoreBuildErrors: false,
   },
🤖 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 `@apps/university-web/next.config.mjs` around lines 63 - 65, The
next.config.mjs currently sets typescript.ignoreBuildErrors: true which can mask
type errors during next build; either set typescript.ignoreBuildErrors to false
in next.config.mjs (update the typescript config entry) or add a clear code
comment and CI-proof justification near the typescript.ignoreBuildErrors setting
referencing the existing CI job (university-web-quality-check / typecheck:ci)
and why this repo still needs true; update the next.config.mjs entry for the
typescript key (typescript.ignoreBuildErrors) accordingly so intent is explicit.
apps/university-web/src/apis/Admin/index.ts-2-5 (1)

2-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

훅 배럴 export 이름에 use 접두사를 유지하세요

  1. apps/university-web/src/apis/Admin/index.ts 배럴은 실제 구현(useGetGpaList, usePutVerifyGpa, useGetLanguageTestList, usePutVerifyLanguageTest)이 훅인데도 getGpaList, putVerifyGpa 같은 이름으로 내보내고 있습니다.
  2. 훅임이 이름에서 바로 드러나도록 use 접두사를 포함한 이름으로 re-export하는 편이 안전합니다.
제안 diff
 export { adminApi } from "./api";
-export { default as getGpaList } from "./getGpaList";
-export { default as getLanguageTestList } from "./getLanguageTestList";
-export { default as putVerifyGpa } from "./putVerifyGpa";
-export { default as putVerifyLanguageTest } from "./putVerifyLanguageTest";
+export { default as useGetGpaList } from "./getGpaList";
+export { default as useGetLanguageTestList } from "./getLanguageTestList";
+export { default as usePutVerifyGpa } from "./putVerifyGpa";
+export { default as usePutVerifyLanguageTest } from "./putVerifyLanguageTest";
🤖 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 `@apps/university-web/src/apis/Admin/index.ts` around lines 2 - 5, The barrel
in Admin/index.ts is re-exporting hooks without the "use" prefix (exporting
getGpaList, putVerifyGpa, getLanguageTestList, putVerifyLanguageTest) even
though the actual implementations are named useGetGpaList, usePutVerifyGpa,
useGetLanguageTestList, usePutVerifyLanguageTest; update the named exports to
re-export using the original hook names (useGetGpaList, useGetLanguageTestList,
usePutVerifyGpa, usePutVerifyLanguageTest) so the exported symbols reflect they
are hooks and match the implementation identifiers.
🧹 Nitpick comments (18)
apps/university-web/src/apis/chat/putReadChatRoom.ts (1)

17-17: ⚡ Quick win

비어 있는 onError가 오류를 조용히 삼키고 있어요.

지금의 onError: (error) => {}는 아무 동작도 하지 않으면서 오류를 그대로 묻어버리고, 사용하지 않는 error 파라미터까지 남겨 lint 경고를 부를 수 있습니다. 핸들러가 정말 필요 없다면 통째로 제거하는 편이 깔끔하고, 디버깅을 위해 흔적을 남기고 싶다면 최소한의 로깅을 넣어주시면 좋겠습니다.

♻️ 불필요한 no-op 핸들러 제거 제안
     onSuccess: () => {
       // 채팅방 목록 쿼리를 무효화하여 새로 고침
       queryClient.invalidateQueries({ queryKey: [ChatQueryKeys.chatRooms] });
     },
-    onError: (error) => {},
   });
🤖 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 `@apps/university-web/src/apis/chat/putReadChatRoom.ts` at line 17, The
anonymous no-op error handler onError: (error) => {} silently swallows errors
and leaves an unused parameter; either remove the onError property entirely from
the options passed to the function (if a handler is not required) or replace it
with a minimal logger that forwards the error (e.g., call console.error or the
module logger) so errors are not swallowed; locate the onError entry in the
options object around putReadChatRoom (or the function constructing the
request/handler) and apply the change.
apps/university-web/src/apis/Auth/postEmailLogin.ts (1)

12-12: 💤 Low value

훅 이름이 동작(이메일 로그인)을 명확히 드러내지 않습니다.

파일명은 postEmailLogin인데 훅은 usePostEmailAuth로 다소 모호합니다. usePostEmailLogin처럼 의도를 분명히 하면 다른 인증 훅들과의 일관성도 좋아집니다.

🤖 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 `@apps/university-web/src/apis/Auth/postEmailLogin.ts` at line 12, Rename the
hook function usePostEmailAuth to usePostEmailLogin to match the file intent and
improve clarity; update the function declaration (usePostEmailAuth →
usePostEmailLogin), any default/named exports that reference usePostEmailAuth,
and all call sites/imports that import/use this hook so they reference
usePostEmailLogin instead; ensure TypeScript types or JSDoc mentioning
usePostEmailAuth are updated as well to avoid unresolved symbol errors.
apps/university-web/src/apis/Auth/postAppleAuth.ts (1)

25-27: ⚖️ Poor tradeoff

setTimeout(100)에 의존한 리다이렉트는 타이밍에 취약합니다.

Zustand persist가 localStorage에 저장될 시간을 100ms로 가정하는 방식은 느린 기기나 환경에서는 충분하지 않을 수 있어 간헐적 인증 누락을 유발할 수 있습니다. 가능하면 저장 완료 시점을 기다리거나(onRehydrateStorage/명시적 await) 토큰을 인메모리로 우선 사용하는 방식이 더 견고합니다.

🤖 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 `@apps/university-web/src/apis/Auth/postAppleAuth.ts` around lines 25 - 27, The
redirect currently relies on a fixed setTimeout(() => router.push("/"), 100)
which is timing-sensitive; replace this by waiting for Zustand persist to finish
or using the in-memory token before redirecting: remove the ad-hoc setTimeout
and either await the store rehydration (use onRehydrateStorage or the persist
promise) or explicitly set the token in-memory in the auth flow and then call
router.push("/") once the store rehydrate/assignment completes; update code
paths in postAppleAuth.ts around the setTimeout/router.push call to perform the
push only after confirming persistence or in-memory token assignment.
apps/university-web/src/apis/Auth/postKakaoAuth.ts (1)

24-31: ⚡ Quick win

100ms 지연 이동은 타이밍에 기댄 임시방편으로 보여요.

토스트가 보일 시간을 벌려고 setTimeout(100) 뒤에 이동시키는 의도는 이해되지만, 이런 매직 딜레이는 기기 성능이나 렌더링 타이밍에 따라 깜빡임이나 토스트 누락으로 이어질 수 있습니다. 토스트 라이브러리의 표시 후 콜백이나 duration 기반 처리로 바꾸면 의도가 더 명확해집니다. 또한 onError에서 /login으로만 보내고 사용자에게 실패 이유를 알리지 않는데, 성공 흐름과 대칭으로 에러 토스트를 함께 노출하는 편이 친절합니다.

♻️ 제안
       onSuccess: (data) => {
         if (data.isRegistered) {
           setAccessToken(data.accessToken);
-          showIconToast("logo", "로그인에 성공했습니다.");
-          setTimeout(() => {
-            router.push("/");
-          }, 100);
+          showIconToast("logo", "로그인에 성공했습니다.");
+          router.push("/");
         } else {
           router.push(`/sign-up?token=${data.signUpToken}`);
         }
       },
-      onError: () => {
-        router.push("/login");
-      },
+      onError: () => {
+        showIconToast("logo", "로그인에 실패했습니다. 다시 시도해 주세요.");
+        router.push("/login");
+      },
🤖 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 `@apps/university-web/src/apis/Auth/postKakaoAuth.ts` around lines 24 - 31,
Replace the fragile setTimeout navigation in the success branch with a
deterministic callback or promise from the toast API: call showIconToast("logo",
"로그인에 성공했습니다.") and when that toast's onClose / .then / duration-complete
handler fires, call router.push("/"); target the block in postKakaoAuth where
showIconToast(...) and setTimeout(...) are used and remove setTimeout. Also
mirror success UX in the error path by calling showIconToast or an error toast
with the failure message before or while calling router.push("/login") inside
the onError branch so the user sees why login failed (update the onError handler
that currently only does router.push("/login")).
apps/university-web/src/apis/kakao-api/index.ts (1)

2-4: 💤 Low value

훅을 getKakaoInfo처럼 동사형 이름으로 재내보내면 헷갈려요 🪪

여기서 내보내는 getKakaoInfo/getKakaoUserIds/postKakaoUnlink는 사실 useGetKakaoInfo 같은 React 훅인데, kakaoApiApi.getKakaoInfo(순수 API 함수)와 이름이 겹쳐 호출부에서 무엇이 훅인지 한눈에 구분하기 어렵습니다. useXxx 형태로 재내보내면 훅 규칙(Rules of Hooks)도 명확해지고 혼동도 줄어듭니다.

♻️ 이름 정리 예시
 export { kakaoApiApi } from "./api";
-export { default as getKakaoInfo } from "./getKakaoInfo";
-export { default as getKakaoUserIds } from "./getKakaoUserIds";
-export { default as postKakaoUnlink } from "./postKakaoUnlink";
+export { default as useGetKakaoInfo } from "./getKakaoInfo";
+export { default as useGetKakaoUserIds } from "./getKakaoUserIds";
+export { default as usePostKakaoUnlink } from "./postKakaoUnlink";
🤖 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 `@apps/university-web/src/apis/kakao-api/index.ts` around lines 2 - 4, The
exported names currently expose React hooks as verbs (getKakaoInfo,
getKakaoUserIds, postKakaoUnlink) which collides with potential plain API
functions; rename the re-exports to hook-style names (e.g., useGetKakaoInfo,
useGetKakaoUserIds, usePostKakaoUnlink) so callers can immediately recognize
they are hooks; update the export statements in this module to export the
default from "./getKakaoInfo" as useGetKakaoInfo (and similarly for
"./getKakaoUserIds" and "./postKakaoUnlink") and then search/replace their
usages across the codebase to import the new hook names.
apps/university-web/src/apis/MyPage/patchInterestedRegionCountry.ts (1)

5-9: ⚡ Quick win

관심 지역 변경 후 프로필 캐시가 그대로 남습니다.

MenteeInfo에는 interestedCountries가 포함되어 있는데, 이 변이는 성공해도 QueryKeys.MyPage.profile 쿼리를 무효화하지 않아요. 그래서 화면에 이전 관심 국가가 잠시 그대로 보일 수 있습니다.

patchProfile처럼 onSettled에서 프로필을 invalidate해 두면 일관성이 유지됩니다.

♻️ 무효화 추가 제안
-import { useMutation } from "`@tanstack/react-query`";
+import { useMutation, useQueryClient } from "`@tanstack/react-query`";
 import type { AxiosError } from "axios";
+import { QueryKeys } from "../queryKeys";
 import { type InterestedRegionCountryRequest, type InterestedRegionCountryResponse, myPageApi } from "./api";

 const usePatchInterestedRegionCountry = () => {
+  const queryClient = useQueryClient();
   return useMutation<InterestedRegionCountryResponse, AxiosError, InterestedRegionCountryRequest>({
     mutationFn: (data) => myPageApi.patchInterestedRegionCountry(data),
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey: [QueryKeys.MyPage.profile] });
+    },
   });
 };
🤖 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 `@apps/university-web/src/apis/MyPage/patchInterestedRegionCountry.ts` around
lines 5 - 9, The mutation usePatchInterestedRegionCountry currently calls
myPageApi.patchInterestedRegionCountry but does not invalidate the profile
cache, so MenteeInfo.interestedCountries stays stale; update
usePatchInterestedRegionCountry to add an onSettled handler that calls
queryClient.invalidateQueries(QueryKeys.MyPage.profile) (similar to
patchProfile) to ensure the profile query is invalidated after success/failure
and the UI reflects updated interestedCountries.
apps/university-web/src/apis/applications/api.ts (1)

6-8: ⚡ Quick win

1. applications 목록 키를 competitors 의미로 두지 않는 편이 안전합니다.

지금 정의된 competitorsApplicationList가 실제로는 지원 목록 조회 훅에서 사용됩니다. 같은 디렉터리의 경쟁자 조회는 이미 QueryKeys.applications.competitors를 쓰고 있어서 applications 도메인 캐시 체계가 둘로 갈리고, 이후 목록 무효화나 갱신을 공용 키 기준으로 걸면 이 쿼리만 빠질 가능성이 큽니다. applicationList처럼 실제 의미에 맞는 키로 바꾸고 공용 QueryKeys 체계에 맞춰 두는 편이 낫습니다.

🤖 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 `@apps/university-web/src/apis/applications/api.ts` around lines 6 - 8, The
ApplicationsQueryKeys constant incorrectly names the applications list key as
competitorsApplicationList which splits cache keys from the shared
QueryKeys.applications namespace; rename the key to applicationList (change the
property name competitorsApplicationList -> applicationList and its string value
to "applicationList") and update all call sites that reference
ApplicationsQueryKeys.competitorsApplicationList to
ApplicationsQueryKeys.applicationList (and ensure any invalidations or usages
align with QueryKeys.applications.* such as
QueryKeys.applications.applicationList) so the applications domain uses a single
consistent cache key.
apps/university-web/src/apis/community/api.ts (1)

69-82: ⚡ Quick win

메서드별 반환 형태가 섞여 있어 호출부에서 혼동이 생길 수 있어요.

getPostListdeletePostAxiosResponse를 통째로 반환하는데, getBoard·getBoardList·getPostDetailres.data만 반환합니다. 같은 communityApi 객체 안에서 규칙이 달라 호출부가 .data 접근 여부를 매번 기억해야 합니다. 한쪽 컨벤션(보통 .data 반환)으로 통일하시면 사용 편의성이 좋아집니다.

Also applies to: 125-127

🤖 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 `@apps/university-web/src/apis/community/api.ts` around lines 69 - 82, The
methods in communityApi currently mix returning raw AxiosResponse (e.g.,
getPostList, deletePost) and unwrapped res.data (getBoardList, getBoard,
getPostDetail), causing caller confusion; update getPostList and deletePost to
await the axios call and return res.data to match the other methods (and update
their return types to Promise<ListPost[]> / appropriate DTO types), ensuring all
communityApi methods consistently return the response body rather than the full
AxiosResponse.
apps/university-web/src/apis/community/getBoard.ts (1)

3-11: ⚡ Quick win

커뮤니티 쿼리 키 소스가 두 군데로 나뉘어 있어요.

이 훅은 QueryKeys.community.board(공통 모듈)를 쓰는데, 같은 도메인의 다른 훅들은 ./apiCommunityQueryKeys(로컬 상수)를 사용합니다. 키 출처가 이원화되면 무효화 대상이 어긋나 캐시 갱신이 누락될 위험이 있으니, 가능하면 한 소스로 통일하시길 권합니다.

또한 enabled: !!boardCodeboardCode가 숫자 0일 때 요청이 비활성화되는데, 시그니처가 string | number를 허용하므로 경계값에서 의도와 달라질 수 있습니다. 문자열 코드만 받는다면 타입을 string으로 좁히는 편이 명확합니다.

🤖 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 `@apps/university-web/src/apis/community/getBoard.ts` around lines 3 - 11, The
hook useGetBoard currently mixes query key sources (QueryKeys.community.board
vs. CommunityQueryKeys in ./api) and uses enabled: !!boardCode which disables
requests for numeric 0; fix by choosing a single canonical query-key source
(switch useGetBoard to use the local CommunityQueryKeys from ./api or update
other hooks to use the shared QueryKeys so all keys align) and clarify the
boardCode type/guard (either change the parameter type to string if only string
codes are allowed, or change the enabled check to a strict null/undefined check
like boardCode != null to allow 0); update references in useGetBoard (queryKey,
queryFn, and enabled) to match the chosen key constant and adjusted type/guard.
apps/university-web/src/apis/community/deletePost.ts (1)

33-33: ⚡ Quick win

catch 블록이 오류를 조용히 삼키고 있어요.

revalidate 실패를 의도적으로 무시하시는 건 이해되지만, 완전히 빈 블록은 디버깅 단서를 남기지 않고 일부 린터(biome noEmptyBlockStatements)에서 경고로 잡힐 수 있습니다. 최소한 의도를 드러내는 주석이나 가벼운 로깅을 남겨 두시길 권합니다.

♻️ 제안 변경
-  } catch (error) {}
+  } catch (error) {
+    // ISR revalidate 실패는 사용자 흐름을 막지 않도록 무시 (필요 시 모니터링 로깅 추가)
+    console.warn("community page revalidate failed", error);
+  }
🤖 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 `@apps/university-web/src/apis/community/deletePost.ts` at line 33, The empty
catch block in the deletePost function is swallowing failures silently; update
the catch to either log the caught error (e.g., console.warn or a
processLogger.warn) with context like "revalidate failed for post deletion" or
add an explicit comment such as "// intentionally ignoring revalidate errors" to
satisfy linters and make the intent clear; locate the catch following the
revalidate/revalidation call inside deletePost and replace the empty block with
a short log statement or explanatory comment that includes the caught error
variable.
apps/university-web/src/apis/community/postCreatePost.ts (1)

13-27: ⚡ Quick win

재검증 실패가 완전히 무음 처리됩니다.

catch (error) {}가 비어 있어, /api/revalidate 호출이 실패하면 ISR 페이지가 오래된 상태로 남아도 아무런 신호가 남지 않습니다.

게시글 생성 흐름 자체를 막을 필요는 없으니 throw까지는 필요 없지만, 디버깅을 위한 최소한의 로그 한 줄은 남겨 주시는 편이 좋겠습니다.

♻️ 최소 로깅 제안
-  } catch (error) {}
+  } catch (error) {
+    console.error("[revalidateCommunityPage] revalidate 요청 실패:", error);
+  }
🤖 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 `@apps/university-web/src/apis/community/postCreatePost.ts` around lines 13 -
27, The catch block swallowing errors for the revalidation fetch must log the
failure; update the catch in the async revalidation code that uses accessToken
and fetch("/api/revalidate") to at minimum call console.error (or the app's
logger) with a short message and the caught error (e.g., "revalidate failed" +
error) so failures are visible while still not throwing and blocking the post
creation flow.
apps/university-web/src/apis/news/postCreateNews.ts (1)

28-35: 💤 Low value

1. 낙관적 썸네일의 object URL이 해제되지 않아요.

URL.createObjectURL로 만든 미리보기 URL은 onSettled의 재조회로 낙관적 항목이 교체된 뒤에도 메모리에 남습니다. 아티클을 자주 추가하는 흐름이라면 URL.revokeObjectURL로 정리해 주면 누수를 막을 수 있어요. (putUpdateNews.ts에도 동일하게 적용됩니다.)

🤖 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 `@apps/university-web/src/apis/news/postCreateNews.ts` around lines 28 - 35,
The optimisticArticle creation uses URL.createObjectURL for the thumbnail but
never revokes it, causing memory leaks; capture the created object URL (the
value assigned to optimisticArticle.thumbnailUrl) in a local variable when
creating optimisticArticle in postCreateNews.ts (and apply the same pattern in
putUpdateNews.ts), then ensure you call URL.revokeObjectURL on that variable in
the mutation's onSettled/onSuccess handler after the optimistic entry is
replaced (or on error), so the temporary object URL is cleaned up; reference
optimisticArticle, URL.createObjectURL and the mutation onSettled handler when
making the change.
apps/university-web/src/apis/news/deleteLikeNews.ts (1)

17-48: ⚡ Quick win

1. 서버 응답으로 좋아요 수치를 다시 맞추는 단계가 빠져 있어요.

이 훅은 낙관적 업데이트와 onError 롤백까지는 잘 처리하고 있어요. 다만 onSettled(또는 onSuccess)에서 invalidateQueries로 재조회하는 단계가 없어서, 서버가 돌려주는 실제 likeCount와 화면에 표시된 값이 미세하게 어긋날 수 있습니다. postLikeNews.ts와도 동일한 상황이니, 두 훅을 함께 맞춰 두면 좋아요/취소를 빠르게 반복할 때 수치 일관성이 더 안정적입니다.

♻️ 제안: 정산 단계 추가
     onError: (_err, _variables, context) => {
       if (context?.previousArticleList) {
         queryClient.setQueryData<ArticleListResponse>(queryKey, context.previousArticleList);
       }
     },
+
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey });
+    },
   });
🤖 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 `@apps/university-web/src/apis/news/deleteLikeNews.ts` around lines 17 - 48,
Add a reconciliation step after the optimistic update by calling
queryClient.invalidateQueries(queryKey) in the mutation's onSettled (or
onSuccess) handler so the UI is refreshed with the server's authoritative
likeCount; update the useMutation block in deleteLikeNews (the mutation using
mutationFn: newsApi.deleteArticleLike) to include an onSettled that invalidates
the same queryKey (mirror the change in postLikeNews.ts as well) to ensure
likeCount consistency after the server response.
apps/university-web/src/apis/mentor/getReceivedMentorings.ts (1)

5-5: 💤 Low value

OFFSET 상수가 api.ts와 중복돼요 🔁

api.ts에도 동일한 OFFSET = 5가 있어 값이 두 곳에서 따로 관리됩니다. 한쪽만 바뀌면 어긋날 수 있으니, 가능하면 api.ts에서 export 해 공유하는 편이 안전합니다.

🤖 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 `@apps/university-web/src/apis/mentor/getReceivedMentorings.ts` at line 5, The
OFFSET constant is duplicated—remove the local const OFFSET in
getReceivedMentorings.ts and instead import the shared OFFSET exported from
api.ts; update getReceivedMentorings.ts to import { OFFSET } from the module
that defines it (the exported symbol in api.ts) and use that imported OFFSET in
any functions (e.g., the function that computes pagination/limit) so the value
is maintained in a single place.
apps/university-web/src/apis/mentor/postApplyMentoring.ts (1)

13-16: 💤 Low value

주석 문구만 살짝 다듬으면 좋겠어요 📝

무효화 대상은 applyMentoringList(신청한 멘토링 목록)인데 주석은 "멘토 목록을 새로고침"이라고 적혀 있어요. 동작 자체는 올바르니, 혼동을 줄이려면 주석을 "신청한 멘토링 목록 새로고침"으로 맞춰 주시면 깔끔합니다.

🤖 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 `@apps/university-web/src/apis/mentor/postApplyMentoring.ts` around lines 13 -
16, The comment above the onSuccess handler is misleading; update the comment
for the invalidate call that targets MentorQueryKeys.applyMentoringList to
accurately say "신청한 멘토링 목록 새로고침" instead of "멘토 목록을 새로고침" so it matches the
behavior around queryClient.invalidateQueries({ queryKey:
[MentorQueryKeys.applyMentoringList] }) in postApplyMentoring.ts.
apps/university-web/src/apis/mentor/api.ts (1)

177-180: 💤 Low value

region 값은 URL 인코딩을 권장해요 🔗

region이 쿼리 문자열에 그대로 끼워집니다. 공백이나 비ASCII 지역명(예: 한글)이 들어오면 쿼리가 깨질 수 있으니 encodeURIComponent로 감싸 두면 안전합니다. getApplyMentoringListverifyStatus도 동일하게 점검해 두시면 좋아요.

♻️ 제안 diff
-    const res = await axiosInstance.get<MentorListResponse>(`/mentors?region=${region}&page=${page}&size=${size}`);
+    const res = await axiosInstance.get<MentorListResponse>(
+      `/mentors?region=${encodeURIComponent(region)}&page=${page}&size=${size}`,
+    );
🤖 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 `@apps/university-web/src/apis/mentor/api.ts` around lines 177 - 180, The
getMentorList function currently inserts region directly into the query string
which can break for spaces or non-ASCII characters; update getMentorList to
URL-encode region using encodeURIComponent when building
`/mentors?region=...&page=...&size=...`, and also check getApplyMentoringList to
wrap verifyStatus (the query parameter) with encodeURIComponent in its request
building so all query values are safely encoded.
apps/university-web/src/apis/mentor/patchConfirmMentoring.ts (1)

11-17: ⚡ Quick win

onSuccess에서 Promise.all을 return/await 하지 않고 있어요 ⏳

지금은 Promise.all([...]) 결과가 그대로 버려져서, 무효화가 완료되기 전에 뮤테이션이 먼저 settle 됩니다. 또한 반환·await 되지 않은 프라미스라 실패 시 unhandled rejection이 될 수 있어요. postApplyMentoring처럼 async/await로 무효화 완료를 기다리도록 맞추면 동작도 일관되고 안전해집니다. 더불어 변수명 queriesClientqueryClient 오타로 보여요.

♻️ 제안 diff
-const usePatchMentorCheckMentorings = () => {
-  const queriesClient = useQueryClient();
-  return useMutation<PatchCheckMentoringsResponse, AxiosError, PatchCheckMentoringsRequest>({
-    onSuccess: () => {
-      // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화
-      Promise.all([
-        queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }),
-        queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }),
-      ]);
-    },
-    mutationFn: mentorApi.patchMentorCheckMentorings,
-  });
-};
+const usePatchMentorCheckMentorings = () => {
+  const queryClient = useQueryClient();
+  return useMutation<PatchCheckMentoringsResponse, AxiosError, PatchCheckMentoringsRequest>({
+    onSuccess: async () => {
+      // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화
+      await Promise.all([
+        queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }),
+        queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }),
+      ]);
+    },
+    mutationFn: mentorApi.patchMentorCheckMentorings,
+  });
+};
🤖 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 `@apps/university-web/src/apis/mentor/patchConfirmMentoring.ts` around lines 11
- 17, onSuccess currently fires Promise.all([...]) without awaiting or returning
it and uses the misspelled queriesClient; change the onSuccess handler to be
async and await/return the Promise.all that invalidates
MentorQueryKeys.mentoringList and MentorQueryKeys.mentoringNewCount so the
mutation waits for invalidation to complete (matching postApplyMentoring
behavior), and rename queriesClient to the correct queryClient identifier.
apps/university-web/src/apis/Scores/api.ts (1)

12-36: 💤 Low value

공개 타입 표면의 작은 불일치 두 가지를 정리하면 좋겠습니다.

거의 쌍둥이 같은 두 흐름인데 이름과 타입이 살짝 엇갈려 있어 눈이 잠깐 멈칫합니다.

  1. 네이밍 규칙
    • UseMyGpaScoreResponse에는 Get이 빠져 있고 UseGetMyLanguageTestScoreResponse에는 들어 있어, 둘 중 하나로 통일하면 자동완성에서 나란히 정렬되어 찾기 쉬워집니다.
  2. file 필드 타입
    • UsePostGpaScoreRequest.fileBlob, UsePostLanguageTestScoreRequest.fileFile로 선언되어 있습니다.
    • FileBlob을 확장하므로 컴파일은 통과하지만, 동일한 의미의 업로드 필드는 같은 타입으로 맞추는 편이 호출부 혼선을 줄여줍니다.
♻️ 일관성을 맞추는 제안
-export interface UseMyGpaScoreResponse {
+export interface UseGetMyGpaScoreResponse {
   gpaScoreStatusResponseList: GpaScore[];
 }
@@
 export interface UsePostGpaScoreRequest {
   gpaScoreRequest: {
     gpa: number;
     gpaCriteria: number;
     issueDate: string; // yyyy-MM-dd
   };
-  file: Blob;
+  file: File;
 }

UseMyGpaScoreResponse를 변경하는 경우 getGpaList.tsindex.ts의 재-export도 함께 맞춰주세요.

🤖 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 `@apps/university-web/src/apis/Scores/api.ts` around lines 12 - 36, Unify the
public types: rename UseMyGpaScoreResponse to UseGetMyGpaScoreResponse (to match
UseGetMyLanguageTestScoreResponse) and make both POST request file fields the
same type by changing UsePostGpaScoreRequest.file from Blob to File (or change
both to Blob if you prefer), then update any re-exports/usages (notably in
getGpaList.ts and index.ts) to the new name; ensure identifiers
UseMyGpaScoreResponse, UseGetMyLanguageTestScoreResponse,
UsePostGpaScoreRequest.file, and UsePostLanguageTestScoreRequest.file are
updated consistently across the codebase.

Comment thread .github/workflows/ci.yml
Comment on lines +76 to +88
- name: Checkout repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
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
# 새로 추가된 university-web 잡의 액션 참조와 checkout 옵션을 점검합니다.
sed -n '69,95p;156,187p' .github/workflows/ci.yml

echo
echo '[uses references]'
rg -n 'uses:\s*(actions/checkout|actions/setup-node|pnpm/action-setup)@' .github/workflows/ci.yml

echo
echo '[checkout blocks]'
rg -n -A3 -B1 'name:\s*Checkout repository' .github/workflows/ci.yml

Repository: solid-connection/solid-connect-web

Length of output: 3441


CI 보안 강화: 액션 참조를 SHA로 고정하고 checkout에 persist-credentials: false 추가하기

  1. university-web-quality-check / university-web-builduses:actions/checkout@v4, pnpm/action-setup@v3, actions/setup-node@v4처럼 태그 기반이라 공급망 리스크가 커서 커밋 SHA로 고정해 주세요.
  2. 두 잡 모두 Checkout repositorypersist-credentials: false가 없어 읽기 전용 체크아웃에서도 불필요한 자격 증명이 유지될 수 있으니 추가해 주세요.
🔧 예시 수정안
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@<PINNED_SHA>
+        with:
+          persist-credentials: false

       - name: Install pnpm
-        uses: pnpm/action-setup@v3
+        uses: pnpm/action-setup@<PINNED_SHA>

       - name: Setup Node.js
-        uses: actions/setup-node@v4
+        uses: actions/setup-node@<PINNED_SHA>
🧰 Tools
🪛 zizmor (1.25.2)

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

(artipacked)


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

(unpinned-uses)


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

(unpinned-uses)


[error] 85-85: 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 76 - 88, Replace the tag-based action
references with pinned commit SHAs for actions/checkout, pnpm/action-setup, and
actions/setup-node in both the university-web-quality-check and
university-web-build jobs to reduce supply-chain risk (e.g., change uses:
actions/checkout@v4 → uses: actions/checkout@<commit-sha>), and add
persist-credentials: false to the Checkout repository step (the actions/checkout
invocation) in both jobs to avoid leaking credentials on read-only checkouts;
update the three uses: lines and the checkout step accordingly wherever they
appear.

Comment thread apps/university-web/.env Outdated
Comment thread apps/university-web/.env.development Outdated
Comment thread apps/university-web/.env.production Outdated
Comment thread apps/university-web/sentry.server.config.ts Outdated
},
} as const;

export type QueryKey = (typeof QueryKeys)[keyof typeof QueryKeys];
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

1) QueryKey 타입이 리프 문자열 유니온을 표현하지 못합니다

- Line 132의 현재 타입은 `Auth | news | ...` 같은 “객체 유니온”으로 평가되어, 실제 쿼리 키 문자열 타입 안정성이 깨집니다. 리프 값 유니온으로 바꿔야 합니다.
수정 예시(diff)
 export type QueryKey = (typeof QueryKeys)[keyof typeof QueryKeys];
+type LeafValues<T> = T extends object ? LeafValues<T[keyof T]> : T;
+export type QueryKey = LeafValues<typeof QueryKeys>;
🤖 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 `@apps/university-web/src/apis/queryKeys.ts` at line 132, The current QueryKey
type evaluates to a union of object shapes rather than the leaf string values;
replace the single-line alias with a recursive leaf-extraction type and then set
QueryKey to the extracted leaf type. Concretely, add a generic recursive helper
(e.g., LeafValues<T>) that resolves to T when T is a string and otherwise
recurses into T[keyof T], then change the QueryKey definition to use
LeafValues<typeof QueryKeys> so QueryKey becomes the union of all leaf string
keys from QueryKeys.


export type AddWishRequest = Record<string, never>;

export type IsWishResponse = undefined;
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

2. isWish 조회가 상태값을 절대 노출하지 못합니다.

Line 53의 IsWishResponse = undefined 때문에 Line 162의 GET 호출은 200이어도 data가 항상 undefined입니다. 이 상태로는 하트 초기 상태를 안정적으로 렌더링할 수 없으니, 백엔드 계약에 맞는 boolean 또는 명시적 객체 타입으로 바꿔 주세요.

Also applies to: 162-169

🤖 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 `@apps/university-web/src/apis/universities/api.ts` at line 53, Change the
incorrect type alias IsWishResponse (currently set to undefined) to match the
backend contract—either a boolean or an explicit object like { isWish: boolean
}—and update the GET request in this file that uses IsWishResponse so its
generic and any consumers read the real boolean field (e.g., replace usages
expecting data to be undefined and instead read data or data.isWish). Ensure the
exported IsWishResponse type and the fetch call signatures in
apps/university-web/src/apis/universities/api.ts are consistent so the heart
initial state can be reliably derived from the response.

Comment on lines +132 to +133
export type ByRegionCountryResponse = undefined;

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

1. 지역/국가 검색 응답 타입이 undefined로 고정돼 있습니다.

Line 132에서 ByRegionCountryResponseundefined로 선언해서, Line 191의 /universities/search 호출은 성공해도 소비자에게 남는 데이터가 없습니다. 검색 결과를 그리는 훅이므로 실제 응답 스키마로 타입을 잡아야 합니다.

Also applies to: 191-193

🤖 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 `@apps/university-web/src/apis/universities/api.ts` around lines 132 - 133,
ByRegionCountryResponse is incorrectly declared as undefined so the
/universities/search consumer gets no typed data; replace that alias with the
actual response shape returned by the search endpoint (e.g., a typed object or
array such as { items: University[]; total: number; page?: number; perPage?:
number } or the existing PaginatedResponse type), update the export of
ByRegionCountryResponse accordingly, and ensure the search caller that consumes
ByRegionCountryResponse (the /universities/search call) and any hooks import the
correct University or paginated response types so the hook can render the
returned data.

Comment on lines +12 to +16
return useMutation<WishResponse, AxiosError, number>({
mutationFn: (universityInfoForApplyId) => universitiesApi.deleteWish({ univApplyInfoId: universityInfoForApplyId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] });
},
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

3. 삭제 후 isWish 캐시도 함께 무효화해 주세요.

지금은 위시리스트만 invalidate해서, 같은 학교를 보고 있는 화면의 useGetIsWish 캐시가 남습니다. 삭제 직후에도 하트가 켜진 채 보일 수 있습니다.

수정 예시
   return useMutation<WishResponse, AxiosError, number>({
     mutationFn: (universityInfoForApplyId) => universitiesApi.deleteWish({ univApplyInfoId: universityInfoForApplyId }),
-    onSuccess: () => {
+    onSuccess: (_, univApplyInfoId) => {
       queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] });
+      queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.isWish, univApplyInfoId] });
     },
   });
🤖 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 `@apps/university-web/src/apis/universities/deleteWish.ts` around lines 12 -
16, The deleteWish mutation currently only invalidates
QueryKeys.universities.wishList, so the cached "isWish" state (used by
useGetIsWish) remains stale; update the onSuccess handler in the useMutation
returned by deleteWish (mutationFn -> universitiesApi.deleteWish) to also
invalidate the isWish cache—call queryClient.invalidateQueries for
QueryKeys.universities.isWish and include the universityInfoForApplyId
(univApplyInfoId) as the identifier so the per-university isWish query is
refreshed immediately after deletion.

queryKey: [QueryKeys.universities.wishList],
queryFn: () => universitiesApi.getWishList({}),
staleTime: 1000 * 60 * 5,
select: (data) => data as unknown as ListUniversity[],
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

위시리스트만 응답 전체를 통째로 캐스팅하고 있어요 — 한 번 짚고 넘어가요.

형제 훅들(getSearchFilter, getSearchText)은 data.univApplyInfoPreviews처럼 내부 배열 필드를 꺼내 쓰는데, 이 훅만 data 전체를 ListUniversity[]로 단언합니다. 만약 WishListResponse가 배열을 감싼 래퍼 객체라면, as unknown as가 타입 오류를 가려버린 탓에 런타임에서 배열이 아닌 객체가 흘러가 .map/.length 사용처가 조용히 깨질 수 있습니다. WishListResponse의 실제 형태를 확인해 주세요.

🛡️ 응답이 배열을 감싼 형태일 경우의 예시
-    select: (data) => data as unknown as ListUniversity[],
+    select: (data) => data.univApplyInfoPreviews as unknown as ListUniversity[],

WishListResponse 타입과 getWishList 응답 구조를 확인합니다.

#!/bin/bash
# WishListResponse 정의와 getWishList 엔드포인트 매핑 확인
fd -t f 'api.ts' apps/university-web/src/apis/universities | xargs -I{} sh -c 'echo "== {} =="; cat "{}"'
rg -nP --type=ts -C3 '\bWishListResponse\b'
🤖 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 `@apps/university-web/src/apis/universities/getWishList.ts` at line 16, The
select handler in getWishList is unsafely casting the entire response (data) to
ListUniversity[] which hides mismatched shapes; inspect the WishListResponse
type and the actual endpoint payload and change the select from "select: (data)
=> data as unknown as ListUniversity[]" to return the proper inner array (e.g.,
data.univApplyInfoPreviews or the actual array field name) and adjust the return
type to ListUniversity[] (remove the as unknown as cast), ensuring getWishList
and any consumers use the correct field and types consistent with
WishListResponse.

…-deploy

chore: 대학 정보 릴리즈 타깃 추가
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: 1

🤖 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/release.yml:
- Line 12: The workflow default for workflow_dispatch.inputs.target is set to
"all", which causes releases to include release-university when left
unspecified; change the workflow to remove or narrow that default (e.g., set to
an explicit subset or no default) and update any calling scripts/docs to always
pass the intended target; inspect and update references to
workflow_dispatch.inputs.target and any invocations (manual dispatch, automation
scripts) to ensure they explicitly provide the desired target values so
university is not promoted unintentionally.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e2593a98-d552-48f3-8d20-4b94699eb7b9

📥 Commits

Reviewing files that changed from the base of the PR and between b3f301a and eae28e7.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment thread .github/workflows/release.yml Outdated
@vercel vercel Bot temporarily deployed to Preview – solid-connection-web June 2, 2026 09:53 Inactive
@manNomi
Copy link
Copy Markdown
Contributor Author

manNomi commented Jun 3, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 845b8921f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const languageTestTypeParam = searchParams.get("languageTestType");
const queryLanguageTestType = isLanguageTestType(languageTestTypeParam) ? languageTestTypeParam : null;
const queryCountryCodesKey = searchParams.getAll("countryCode").filter(isCountryCode).join(",");
const queryRegion = searchParams.getAll("region").filter(isRegionFilterValue).at(0) ?? RegionEnumExtend.ALL;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve all selected region filters

When users submit the advanced school search with multiple regions selected, buildUniversitySearchQuery appends every selected region, but this page collapses searchParams.getAll("region") to only .at(0). As a result /university/inha?region=유럽권&region=미주권 shows only the first region instead of the union of selected regions, which regresses the multi-region filter behavior that this form still exposes.

Useful? React with 👍 / 👎.

@vercel vercel Bot temporarily deployed to Preview – solid-connect-web-admin June 3, 2026 07:17 Inactive
@vercel vercel Bot temporarily deployed to Preview – solid-connection-web June 3, 2026 07:17 Inactive
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.

대학 상세 페이지 정적 생성으로 인한 웹 빌드 시간 개선

1 participant