Skip to content

feat: 어드민 대학 지원 정보 관리 기능 추가#558

Merged
whqtker merged 33 commits into
mainfrom
feat/admin-univ-apply-info
Jun 18, 2026
Merged

feat: 어드민 대학 지원 정보 관리 기능 추가#558
whqtker merged 33 commits into
mainfrom
feat/admin-univ-apply-info

Conversation

@whqtker

@whqtker whqtker commented Jun 17, 2026

Copy link
Copy Markdown
Member

작업 내용

  • 어드민에 협정 대학, 지원 대학, 학기 관리 화면을 추가했습니다.
  • 지원 대학 엑셀 임포트의 컬럼 매핑, 국가 코드 자동 변환, 셀 단위 검증, 미리보기 모달을 개선했습니다.
  • 서버 응답 타입 변경에 맞춰 failedRows 의존 UI를 정리하고 관련 검증 테스트를 업데이트했습니다.

검증

  • 브랜치에 포함된 커밋 기준으로 검증됨: 셀 검증/필드 매핑/학기·대학 관리 관련 테스트 및 타입 정리 커밋 포함
  • 이 PR 생성 단계에서는 추가 로컬 테스트를 실행하지 않았습니다.

whqtker added 27 commits June 15, 2026 15:56
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

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 18, 2026 5:52am
solid-connect-web-admin Ready Ready Preview, Comment Jun 18, 2026 5:52am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
solid-connection-web Skipped Skipped Jun 18, 2026 5:52am

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@whqtker, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 51 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5b7ddbd0-e11f-4cbb-b9d5-1e19ef1aa778

📥 Commits

Reviewing files that changed from the base of the PR and between 94266fe and d380e5c.

📒 Files selected for processing (3)
  • apps/admin/src/components/features/univ-apply-infos/UnivApplyInfosPageContent.tsx
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoImportGuard.ts
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoValidation.ts

Walkthrough

이번 PR은 admin 앱에 세 가지 주요 기능을 신규로 추가합니다.

  1. 협정 대학 관리

    • adminApi에 협정 대학 CRUD 엔드포인트 추가
    • HomeUniversitiesPageContent에서 목록 조회·인라인 수정·삭제 구현
    • HomeUniversitiesPage 진입점으로 세션 보호 연결
  2. 학기 관리

    • normalizeTermName 검증 함수 및 Vitest 단위 테스트 추가
    • TermsPageContent에서 학기 생성·활성화 구현
    • TermsPage 진입점으로 세션 보호 연결
  3. 지원정보 일괄 임포트

    • 국가명→ISO 코드 매핑 상수 및 마크다운 전처리 유틸 추가
    • 필드 스키마 상수·헤더 매핑·행 파싱·셀 검증 유틸 추가
    • UnivApplyInfosPageContent에서 마크다운 파싱→컬럼 매핑→미리보기 모달→서버 임포트 전체 흐름 구현

추가로 사이드바에 세 메뉴 항목 등록, Vite에 next/navigation shim alias 추가, .gitignoredocs/superpowers/ 경로 등록, apps/web의 next 버전 소폭 업그레이드가 포함됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • wibaek
  • enunsnv
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항(어드민 대학 지원 정보 관리 기능 추가)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 작업 내용과 검증 사항을 포함하고 있으나, 템플릿의 '관련 이슈' 섹션이 누락되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-univ-apply-info

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.

@whqtker whqtker self-assigned this Jun 17, 2026
@whqtker whqtker changed the title 어드민 대학 지원 정보 관리 기능 추가 feat: 어드민 대학 지원 정보 관리 기능 추가 Jun 17, 2026
Comment thread apps/web/package.json
"linkifyjs": "^4.3.2",
"lucide-react": "^0.479.0",
"next": "^16.2.6",
"next": "^16.2.9",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

순수 궁금증 이거 패치하신 이유가 있나요 ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

정확한 원인은 기억이 잘 나지 않는데, 개발 서버 실행이 실패해서 이것저것 만지다가 들어간 거 같습니다.

롤백해도 어드민 정상 동작하는 거 확인했고, 롤백했습니다 !

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

제가 ai 셋업할때 어드민은 어드민만 수정하게 좀 유도를 스킬화 해놓은것 같은데
"그게 잘 안작동했나?"하는 생각이 들어서 그랬습니다
혹시 하네스 설정 해보고 싶은거 있으면 추가해도 좋습니다 같이 얘기해도 좋고요

@whqtker whqtker Jun 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

아 그런가요 ? 그렇다면 그 부분 관련해서 저도 한 번 살펴보겠습니다 !! 어드민 쪽 웹 만질 일이 많을 거 같아서요
작업 당시에는 별도의 스킬이 호출되거나 그러진 않았던 거로 기억합니다

@@ -0,0 +1,133 @@
export const COUNTRY_CODE_BY_NAME: Record<string, string> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

따로 빼도 좋을것 같아요 상위에 constant 만들어서!

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8cf689fa07

ℹ️ 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".

toast.error("마크다운 헤더를 파싱할 수 없습니다. 형식을 확인해주세요.");
return;
}
const auto = buildAutoMappings(headers, fieldsQuery.data?.languageTestTypes ?? []);

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 Disable parsing until field metadata is loaded

When an admin clicks 파싱 before fieldsQuery has returned, this falls back to an empty languageTestTypes list, so headers such as TOEIC/TOEFL are never written to columnMappings. The later query success does not recompute mappings and the language rows are rendered read-only, so the import can proceed without mapping those score columns and the bulk import loses/ignores the language-test data from that paste.

Useful? React with 👍 / 👎.

Comment on lines +96 to +100
for (const row of rows) {
for (const [field, cell] of Object.entries(row.cellsByField)) {
const rules = FIELD_RULES[field];
if (!rules) continue;
const message = validateCell(cell.value, rules);

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 Validate missing required import fields

In cases where a required column is not mapped, or a row lacks a cell for that mapped column, it is absent from row.cellsByField, so this loop never applies any required-field rule and clientCellErrors remains empty. A table like the placeholder can be previewed with required columns shown as and still enable the final 추가 button, causing the server import to fail with only the generic mutation error instead of blocking the invalid preview.

Useful? React with 👍 / 👎.

Comment on lines +5 to +6
it("contains only required system fields", () => {
expect(UNIV_APPLY_INFO_FIELDS.every((field) => field.required)).toBe(true);

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 Fix the contradictory field test

This assertion currently fails because UNIV_APPLY_INFO_FIELDS also includes universityHomepageUrl with required: false, so any run of the admin Vitest suite will fail before exercising the import logic. Either keep this list required-only or update the test to allow the optional homepage field.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
apps/admin/src/components/features/home-universities/HomeUniversitiesPageContent.tsx (2)

114-121: ⚡ Quick win

접근성 개선: 폼 입력 필드에 aria-label 추가 권장

현재 생성 폼의 Input 컴포넌트들에 명시적인 레이블이 없습니다. 다음을 권장합니다:

  1. 대학명 입력 (Line 114): aria-label="대학명" 추가
  2. 최대 지망 수 입력 (Line 115): aria-label="최대 지망 수" 추가

참고로 TermsPageContent.tsx Line 87에서는 aria-label="학기 이름"을 이미 사용하고 있어, 일관성을 맞추면 스크린 리더 사용자에게 더 나은 경험을 제공할 수 있습니다.

♻️ 제안 수정안
-						<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="대학명" />
+						<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="대학명" aria-label="대학명" />
 						<Input
 							value={maxChoiceCount}
 							onChange={(e) => setMaxChoiceCount(e.target.value)}
 							placeholder="최대 지망 수"
 							type="number"
 							min={1}
+							aria-label="최대 지망 수"
 						/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/admin/src/components/features/home-universities/HomeUniversitiesPageContent.tsx`
around lines 114 - 121, The Input components in the home universities form lack
explicit accessibility labels for screen readers. Add aria-label attributes to
both Input components to improve accessibility: add aria-label="대학명" to the
Input component that has placeholder "대학명" and the setName handler, and add
aria-label="최대 지망 수" to the Input component that has placeholder "최대 지망 수" and
the setMaxChoiceCount handler with type="number". This will make the form more
accessible to screen reader users and maintain consistency with the pattern
already used in TermsPageContent.tsx.

90-93: ⚖️ Poor tradeoff

선택적 개선: 확인 모달 고려

현재 window.confirm을 사용하여 삭제를 확인하고 있습니다. 기능적으로 문제는 없으나, 다음을 고려해 보세요:

  1. 브라우저 기본 confirm 대화상자는 스타일링이 불가능하고 UX가 일관되지 않을 수 있습니다
  2. 커스텀 모달 컴포넌트로 교체하면 브랜딩과 접근성을 개선할 수 있습니다

다만 관리자 도구에서는 현재 구현도 충분히 실용적이므로, 우선순위에 따라 결정하시면 됩니다.

🤖 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/admin/src/components/features/home-universities/HomeUniversitiesPageContent.tsx`
around lines 90 - 93, The handleDelete function currently uses window.confirm
for deletion confirmation, which cannot be styled and provides inconsistent UX.
Consider replacing the window.confirm call with a custom confirmation modal
component that matches your application's design system and provides better
accessibility. This would involve creating or using an existing modal component
in your project, passing the university name and ID to it, and calling
deleteMutation.mutate(id) only after the user confirms in the custom modal
instead of in the confirm callback.
🤖 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
`@apps/admin/src/components/features/univ-apply-infos/univApplyInfoFields.test.ts`:
- Around line 5-7: The test assertion in "contains only required system fields"
currently expects all fields in UNIV_APPLY_INFO_FIELDS to have required: true,
but the actual constant definition contains fields with required: false, causing
the test to fail. Update the test to properly validate the field structure by
checking that fields are correctly categorized as either required or optional,
rather than asserting that all fields must be required. This can be done by
splitting the test logic to separately validate required fields and optional
fields within UNIV_APPLY_INFO_FIELDS, or by adjusting the assertion to match the
actual schema structure where some fields have required: false.

In
`@apps/admin/src/components/features/univ-apply-infos/UnivApplyInfosPageContent.tsx`:
- Around line 419-423: The Button component's disabled condition does not
prevent users from importing when preview data is empty. Add a check for
previewRows.length === 0 to the disabled prop alongside the existing conditions
for importMutation.isPending and clientCellErrors.size > 0. Additionally, add a
guard condition at the beginning of the handleConfirmImport function to return
early if previewRows is empty, preventing any empty import requests from being
processed.

In
`@apps/admin/src/components/features/univ-apply-infos/univApplyInfoValidation.ts`:
- Around line 23-59: The FIELD_RULES object is missing required field validation
for multiple fields that should be mandatory. Add a type "required" rule with an
appropriate error message to all fields that must not be empty (such as
universityKoreanName should serve as a template). Additionally, verify that the
validation implementation in the code around lines 93-105 properly checks for
empty, null, and undefined values when processing "required" type rules to
ensure invalid rows with missing required fields are caught before import.

In `@apps/admin/src/lib/api/admin.ts`:
- Around line 76-84: The TermCreatePayload interface uses a "name" field while
the TermResponse interface uses a "label" field for the same concept, creating
inconsistency in the API contract. To improve clarity and maintainability,
standardize the field name across both interfaces. Choose either "label" or
"name" and update both TermCreatePayload and TermResponse to use the same field
name consistently. Since TermsPageContent.tsx already references term.label,
aligning TermCreatePayload to use "label" instead of "name" would be the
recommended approach to maintain consistency throughout the codebase.

In `@apps/admin/vite.config.ts`:
- Around line 7-14: The hardcoded path in vinextNavShim does not work correctly
in workspace environments with varying module layouts. Replace the hardcoded
node_modules/vinext/dist/shims/navigation.js path with
require.resolve('vinext/dist/shims/navigation.js') to dynamically resolve the
actual location of the navigation shim. This ensures the vite configuration
works consistently across development, build, and CI environments regardless of
dependency installation method.

---

Nitpick comments:
In
`@apps/admin/src/components/features/home-universities/HomeUniversitiesPageContent.tsx`:
- Around line 114-121: The Input components in the home universities form lack
explicit accessibility labels for screen readers. Add aria-label attributes to
both Input components to improve accessibility: add aria-label="대학명" to the
Input component that has placeholder "대학명" and the setName handler, and add
aria-label="최대 지망 수" to the Input component that has placeholder "최대 지망 수" and
the setMaxChoiceCount handler with type="number". This will make the form more
accessible to screen reader users and maintain consistency with the pattern
already used in TermsPageContent.tsx.
- Around line 90-93: The handleDelete function currently uses window.confirm for
deletion confirmation, which cannot be styled and provides inconsistent UX.
Consider replacing the window.confirm call with a custom confirmation modal
component that matches your application's design system and provides better
accessibility. This would involve creating or using an existing modal component
in your project, passing the university name and ID to it, and calling
deleteMutation.mutate(id) only after the user confirms in the custom modal
instead of in the confirm callback.
🪄 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: d367e552-f503-41e1-bbd4-8df0779d3f00

📥 Commits

Reviewing files that changed from the base of the PR and between 430af70 and d4b5ec2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .gitignore
  • apps/admin/src/app/home-universities/page.tsx
  • apps/admin/src/app/terms/page.tsx
  • apps/admin/src/app/univ-apply-infos/page.tsx
  • apps/admin/src/components/features/home-universities/HomeUniversitiesPageContent.tsx
  • apps/admin/src/components/features/terms/TermsPageContent.tsx
  • apps/admin/src/components/features/terms/termValidation.test.ts
  • apps/admin/src/components/features/terms/termValidation.ts
  • apps/admin/src/components/features/univ-apply-infos/UnivApplyInfosPageContent.tsx
  • apps/admin/src/components/features/univ-apply-infos/countryCodeAliases.ts
  • apps/admin/src/components/features/univ-apply-infos/countryCodeConstants.ts
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoFields.test.ts
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoFields.ts
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoPreview.ts
  • apps/admin/src/components/features/univ-apply-infos/univApplyInfoValidation.ts
  • apps/admin/src/components/layout/AdminSidebar.tsx
  • apps/admin/src/lib/api/admin.ts
  • apps/admin/vite.config.ts
  • apps/web/package.json

Comment thread apps/admin/src/components/features/univ-apply-infos/UnivApplyInfosPageContent.tsx Outdated
Comment thread apps/admin/src/lib/api/admin.ts
Comment thread apps/admin/vite.config.ts
Comment on lines +7 to +14
const vinextNavShim = fileURLToPath(new URL("node_modules/vinext/dist/shims/navigation.js", import.meta.url));

const config = defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"next/navigation": vinextNavShim,
"next/navigation.js": vinextNavShim,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
node --input-type=module - <<'NODE'
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);

try {
  console.log(require.resolve('vinext/dist/shims/navigation.js'));
} catch (err) {
  console.error('vinext shim could not be resolved through package resolution');
  process.exit(1);
}
NODE

Repository: solid-connection/solid-connect-web

Length of output: 139


🏁 Script executed:

cat -n apps/admin/vite.config.ts | head -30

Repository: solid-connection/solid-connect-web

Length of output: 896


🏁 Script executed:

fd -type f -name "package.json" | xargs -I {} grep -l "vinext" {} 2>/dev/null || echo "No vinext found in package.json files"

Repository: solid-connection/solid-connect-web

Length of output: 249


🏁 Script executed:

find . -name "package.json" -type f | head -10 | xargs grep -l "vinext" 2>/dev/null || echo "No vinext dependency found"

Repository: solid-connection/solid-connect-web

Length of output: 104


🏁 Script executed:

cat package.json | grep -A 10 "vinext" || cat apps/admin/package.json | grep -A 10 "vinext" || echo "Checking root and app package.json"

Repository: solid-connection/solid-connect-web

Length of output: 771


Vinext 경로를 동적으로 해석하도록 변경해주세요.

현재 코드에서 node_modules/vinext/dist/shims/navigation.js를 하드코딩한 경로는 워크스페이스 환경에서 올바르게 해석되지 않습니다. 아래와 같이 개선해야 합니다:

  1. 패키지 해석기 활용

    • Node의 require.resolve()를 사용하여 경로를 동적으로 가져오기
    • 이렇게 하면 pnpm 호이스팅, 중첩 설치 등 다양한 모듈 배치에 대응 가능
  2. 개선 예시

    • require.resolve('vinext/dist/shims/navigation.js')로 현재 하드코딩 대체
    • 또는 vinext의 공식 API가 있다면 그 방식 따르기
  3. 안정성 확보

    • 개발 환경, 빌드 환경, CI 환경 모두에서 일관되게 작동
    • 의존성 설치 방식이 변경되어도 영향받지 않음
🤖 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/admin/vite.config.ts` around lines 7 - 14, The hardcoded path in
vinextNavShim does not work correctly in workspace environments with varying
module layouts. Replace the hardcoded
node_modules/vinext/dist/shims/navigation.js path with
require.resolve('vinext/dist/shims/navigation.js') to dynamically resolve the
actual location of the navigation shim. This ensures the vite configuration
works consistently across development, build, and CI environments regardless of
dependency installation method.

@vercel vercel Bot temporarily deployed to Preview – solid-connection-web June 18, 2026 05:50 Inactive
@whqtker whqtker merged commit d35d112 into main Jun 18, 2026
13 checks passed
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.

2 participants