Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[YS-243] 홈화면 & 로그인 & 회원가입 스타일링 vanilla-extract로 마이그레이션 #44

Merged
merged 11 commits into from
Feb 5, 2025

Conversation

rbgksqkr
Copy link
Contributor

@rbgksqkr rbgksqkr commented Feb 4, 2025

Issue Number

close #37

As-Is

  • emotion으로 인한 서버사이드 동작 어려움

To-Be

  • zero runtime css인 vanila extract로 마이그레이션
    • 로그인
      • 구글 로그인
      • 네이버 로그인
    • 회원가입
      • 참여자 회원가입
      • 연구자 회원가입
    • 홈화면

Check List

  • 테스트가 전부 통과되었나요?
  • 모든 commit이 push 되었나요?
  • merge할 branch를 확인했나요?
  • Assignee를 지정했나요?
  • Label을 지정했나요?

Test Screenshot

(Optional) Additional Description

마이그레이션이 생각보다 오래걸렸네요... 어려운 스타일링 코드가 있진 않았는데 이 작업으로 인해 다른 게 좀 많이 딜레이 됐네요 ㅠ 빠르게 작업해보겠습니다!

Summary by CodeRabbit

  • 스타일 개선

    • 배너, 카드, 필터, 회원가입/로그인 페이지 등 전반적인 인터페이스의 레이아웃, 애니메이션, 반응형 디자인이 개선되었습니다.
    • 사용자 인터랙션 시 보다 매끄러운 전환 효과와 동적 피드백을 제공합니다.
  • 리팩토링

    • 기존 CSS‑in‑JS 방식에서 CSS 모듈 기반의 클래스 적용으로 전환하여 유지보수성과 성능이 향상되었습니다.
    • 전체 스타일 적용 방식이 일관되고 최적화되어 사용자 경험이 개선되었습니다.

@rbgksqkr rbgksqkr added refactor 코드 리팩토링 design css와 같은 스타일링 labels Feb 4, 2025
@rbgksqkr rbgksqkr requested a review from eeeyooon February 4, 2025 16:55
@rbgksqkr rbgksqkr self-assigned this Feb 4, 2025
Copy link

vercel bot commented Feb 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
25th-web-team-2-fe ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 5, 2025 11:58am

Copy link

coderabbitai bot commented Feb 4, 2025

Walkthrough

이번 변경사항은 Emotion 기반 CSS-in-JS 스타일을 vanilla-extract 기반의 CSS 모듈로 전환하는 작업입니다. 홈화면, 로그인, 회원가입, 검색 및 기타 관련 컴포넌트에서 기존의 .styles.ts 파일들을 삭제하고, 새로운 .css.ts 파일들을 추가하였으며, 스타일 적용 방식을 css prop에서 className prop으로 변경하였습니다. 또한, 몇몇 컴포넌트에서는 assignInlineVars를 사용해 동적 CSS 변수 적용을 도입하였습니다.

Changes

File(s) Change Summary
src/app/home/components/Banner/* Banner 컴포넌트에서 새로운 .css.ts 파일 추가, 기존 .styles.ts 삭제 및 cssclassName 전환
src/app/home/components/PostCard/* PostCard 컴포넌트에 대해 새로운 CSS 모듈 파일 추가, 기존 Emotion 스타일 파일 삭제, 스타일 적용 방식 변경
src/app/home/components/PostCardList/* EmptyPostCard 및 PostCardList에 대해 새로운 CSS 모듈 파일 추가, 기존 .styles.ts 파일 삭제, css prop 대신 className 사용
src/app/home/components/PostContainer/* AreaFilter, ContactTargetPopover, FilterContainer, PostContainer, ProgressMethodFilter 등 컴포넌트에서 vanilla-extract CSS 모듈 도입, 기존 Emotion 스타일 삭제 및 사용 방식 전환
src/app/join/** JoinPage, EmailToast, JoinCheckbox, JoinInput, JoinSuccessStep, JoinInfoStep, JoinSelect 등 회원가입 관련 컴포넌트에서 CSS-in-JS를 vanilla-extract로 마이그레이션, cssclassName 전환 및 파일명 변경
src/app/login/** LoginPage, LoginCard, GoogleLoginPage, NaverLoginPage 및 레이아웃에서 vanilla-extract 기반의 CSS 모듈 도입, 기존 Emotion 스타일 파일 삭제 및 클래스 기반 스타일 적용

Sequence Diagram(s)

sequenceDiagram
    participant JoinPage
    participant ResearcherForm
    participant ParticipantForm
    JoinPage->>JoinPage: 현재 step 체크 (예: step === success)
    alt step이 success인 경우
        JoinPage->>ResearcherForm: 연구자용 폼 렌더링 (role이 Researcher인 경우)
        JoinPage->>ParticipantForm: 참여자용 폼 렌더링 (role이 Participant인 경우)
    else
        JoinPage->>JoinPage: 기본 회원가입 폼 렌더링
    end
Loading

Assessment against linked issues

Objective (관련 이슈) Addressed Explanation
홈화면, 로그인, 회원가입 스타일링을 vanilla-extract 전환 (#37)
필터링 및 기타 UI 컴포넌트의 스타일링을 vanilla-extract 적용 (#37)
구글/네이버 로그인 및 연구자/참여자 회원가입 스타일링 적용 (#37)

Possibly related PRs

Suggested labels

feat, chore

나는 작은 토끼, 스타일의 길을 달리네,
Emotion의 잔향을 뒤로 하고,
vanilla-extract로 새로운 색을 칠하며,
코드의 숲 속을 날아다니네,
변화의 바람 속에서 경쾌한 발자국을 남기네. 🐇🌸

Tip

🌐 Web search-backed reviews and chat
  • We have enabled web search-based reviews and chat for all users. This feature allows CodeRabbit to access the latest documentation and information on the web.
  • You can disable this feature by setting web_search: false in the knowledge_base settings.
  • Please share any feedback in the Discord discussion.
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

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

🔭 Outside diff range comments (5)
src/app/join/components/JoinInput/JoinInput.tsx (1)

87-127: 접근성 개선이 필요합니다.

입력 필드의 접근성을 높이기 위해 다음과 같은 개선사항을 제안드립니다:

 <div className={inputWrapper}>
   {type === 'input' ? (
     <input
       {...field}
       ref={inputRef}
       placeholder={placeholder}
       disabled={disabled}
       maxLength={maxLength}
       aria-invalid={fieldState.invalid ? true : false}
+      aria-describedby={`${name}-error ${name}-tip`}
       style={{ width: '100%' }}
       className={joinInput}
       onFocus={() => setIsFocused(true)}
       onBlur={(e) => handleBlur(e, field.onBlur)}
     />
   ) : (
     <textarea
       {...field}
       ref={textareaRef}
       placeholder={placeholder}
       disabled={disabled}
       aria-invalid={fieldState.invalid ? true : false}
+      aria-describedby={`${name}-error ${name}-tip`}
       rows={3}
       maxLength={maxLength ?? 0}
       style={{ width: '100%' }}
       className={joinInput}
       onFocus={() => setIsFocused(true)}
       onBlur={(e) => handleBlur(e, field.onBlur)}
     />
   )}
-  {isFocused && field.value && !disabled && (
+  {isFocused && field.value && !disabled && (
     <button 
       className={inputResetButton} 
       ref={resetButtonRef}
+      aria-label="입력 내용 지우기"
+      type="button"
     >
🧰 Tools
🪛 Biome (1.9.4)

[error] 95-95: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)


[error] 107-107: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

src/app/login/components/LoginCard/LoginCard.tsx (1)

35-43: 보안 및 유지보수성 개선이 필요합니다.

OAuth URL 생성 로직에 대한 몇 가지 개선사항을 제안드립니다:

  1. OAuth URL 생성 로직을 별도의 유틸리티 함수로 분리
  2. 환경 변수 존재 여부 검증 추가

다음과 같이 개선해보세요:

+const validateEnvVariables = () => {
+  const required = [
+    'NEXT_PUBLIC_GOOGLE_CLIENT_ID',
+    'NEXT_PUBLIC_GOOGLE_REDIRECT_URI',
+    'NEXT_PUBLIC_NAVER_CLIENT_ID',
+    'NEXT_PUBLIC_NAVER_REDIRECT_URI',
+    'NEXT_PUBLIC_NAVER_STATE'
+  ];
+  
+  const missing = required.filter(key => !process.env[key]);
+  if (missing.length > 0) {
+    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
+  }
+};

+const createGoogleOAuthURL = (role: Role) => {
+  validateEnvVariables();
+  return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
+    process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID
+  }&redirect_uri=${
+    process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI
+  }&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email&state=${
+    roleMapper[role]
+  }`;
+};

+const createNaverOAuthURL = (role: Role) => {
+  validateEnvVariables();
+  return `https://nid.naver.com/oauth2.0/authorize?client_id=${
+    process.env.NEXT_PUBLIC_NAVER_CLIENT_ID
+  }&response_type=code&redirect_uri=${
+    process.env.NEXT_PUBLIC_NAVER_REDIRECT_URI
+  }&state=${
+    process.env.NEXT_PUBLIC_NAVER_STATE
+  }|${
+    roleMapper[role]
+  }`;
+};

 const goToLoginGoogle = () => {
-  const googleOauthURL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI}&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email&state=${roleMapper[role]}`;
+  const googleOauthURL = createGoogleOAuthURL(role);
   router.push(googleOauthURL);
 };

 const goToLoginNaver = () => {
-  const naverOauthURL = `https://nid.naver.com/oauth2.0/authorize?client_id=${process.env.NEXT_PUBLIC_NAVER_CLIENT_ID}&response_type=code&redirect_uri=${process.env.NEXT_PUBLIC_NAVER_REDIRECT_URI}&state=${process.env.NEXT_PUBLIC_NAVER_STATE}|${roleMapper[role]}`;
+  const naverOauthURL = createNaverOAuthURL(role);
   router.push(naverOauthURL);
 };
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx (1)

18-19: any 타입 사용을 피하고 명시적인 이벤트 타입을 사용해주세요.

eslint-disable 주석과 함께 any 타입을 사용하고 있습니다.

다음과 같이 명시적인 이벤트 타입을 사용해주세요:

interface JoinCheckboxProps {
  label: string;
  isChecked: boolean;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  onChange: (e: any) => void;
+  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  isRequired?: boolean;
  isAllCheck?: boolean;
  isArrow?: boolean;
  isAlert?: boolean;
}
src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.tsx (1)

18-20: 대면/비대면 값이 잘못 매핑되어 있습니다.

현재 구현에서 대면은 "ONLINE"으로, 비대면은 "OFFLINE"으로 설정되어 있어 직관적이지 않습니다.

다음과 같이 수정해주세요:

  { label: '전체', value: 'ALL' },
-  { label: '대면', value: 'ONLINE' },
-  { label: '비대면', value: 'OFFLINE' },
+  { label: '대면', value: 'OFFLINE' },
+  { label: '비대면', value: 'ONLINE' },
src/app/home/components/PostContainer/AreaFilter/AreaFilter.tsx (1)

58-64: TODO 주석 관련 개선이 필요합니다.

여러 개의 하위 지역 선택 기능이 아직 구현되지 않았습니다. 현재는 첫 번째 선택된 항목만 처리되고 있습니다.

다음과 같이 수정을 제안합니다:

- onChange('areas', selectedSubArea[0]);
+ onChange('areas', selectedSubArea.join(','));
🧹 Nitpick comments (60)
src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.tsx (1)

14-18: className prop으로의 전환이 잘 이루어졌습니다.

css prop에서 className prop으로의 전환이 올바르게 이루어졌으며, 스타일 적용 방식이 vanilla-extract의 권장 사항을 잘 따르고 있습니다.

다만, 접근성 향상을 위해 다음과 같은 개선을 제안드립니다:

  • 검색 결과가 없다는 것을 스크린 리더에게 더 명확하게 전달하기 위해 aria-label 추가
-    <div className={emptyPostCardLayout}>
+    <div className={emptyPostCardLayout} aria-label="검색 결과 없음">
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (1)

24-24: className 문자열 포맷팅 개선이 필요합니다.

현재 템플릿 리터럴 내부에 불필요한 공백과 줄바꿈이 있어 가독성이 떨어집니다. 조건부 클래스네임을 더 간결하게 작성할 수 있습니다.

다음과 같이 개선해보세요:

-          className={`
-            ${customRadioButton}
-            ${selectedValue === option.value ? activeRadioButton : ''}
-            ${isError ? errorRadioButton : ''}
-          `}
+          className={`${customRadioButton} ${selectedValue === option.value ? activeRadioButton : ''} ${isError ? errorRadioButton : ''}`}

또는 clsx 라이브러리를 사용하여 더 깔끔하게 작성할 수 있습니다:

import clsx from 'clsx';

// ...
className={clsx(
  customRadioButton,
  selectedValue === option.value && activeRadioButton,
  isError && errorRadioButton
)}

Also applies to: 29-33

src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.css.ts (1)

6-16: 스타일 구현이 체계적입니다.

중앙 집중식 테마 시스템을 활용하고 일관된 단위를 사용한 점이 좋습니다.

스타일의 목적과 사용법을 설명하는 간단한 JSDoc 주석을 추가하면 좋을 것 같습니다:

+/**
+ * 약관 동의 체크박스들을 포함하는 컨테이너의 스타일입니다.
+ * 체크박스들을 세로로 배치하고 일관된 간격을 유지합니다.
+ */
export const termContainer = style({
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (1)

5-7: 스타일 관련 import 구문의 구조화가 필요합니다.

스타일 관련 import 구문들이 서로 다른 경로에서 가져오고 있습니다. 코드의 일관성과 유지보수성을 위해 스타일 import를 그룹화하는 것이 좋습니다.

다음과 같이 스타일 관련 import를 그룹화하는 것을 제안합니다:

 import { useFormContext, useWatch } from 'react-hook-form';
 
 import JoinInput from '@/app/join/components/JoinInput/JoinInput';
+
 import { joinButton } from '@/app/join/components/Participant/JoinInfoStep/JoinInfoStep.css';
 import { joinContentContainer, joinForm } from '@/app/join/JoinPage.css';
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx (1)

60-85: 인라인 스타일을 CSS 모듈로 이동하는 것이 좋습니다.

인라인 스타일로 적용된 width 속성을 CSS 모듈로 이동하면 스타일 관리가 더 용이해질 것 같습니다.

다음과 같이 수정하는 것을 제안드립니다:

  1. JoinInput.css.ts에 width 스타일 추가:
export const joinInput = style({
+  width: '100%',
  // ... 기존 스타일
});
  1. 컴포넌트에서 인라인 스타일 제거:
<input
-  style={{ width: '100%' }}
  className={joinInput}
  // ... 나머지 props
/>
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (1)

41-42: 스타일링 적용 방식이 성공적으로 변경되었습니다.

css prop에서 className prop으로의 전환이 올바르게 이루어졌습니다. 다만, 재사용 가능한 스타일의 경우 별도의 상수로 분리하는 것을 고려해보시기 바랍니다.

자주 사용되는 스타일의 경우 다음과 같이 분리하는 것을 추천드립니다:

// constants/styles.css.ts
export const commonButtonStyle = style({
  // ... 공통 버튼 스타일
});

Also applies to: 65-65

src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (1)

24-24: 스타일 적용 방식에 대한 제안

현재 구현은 잘 되어 있으나, 오픈/에러 상태에 따른 스타일 변화를 더 효과적으로 관리할 수 있습니다.

다음과 같이 vanilla-extract의 recipe를 활용하여 상태별 스타일을 관리하는 것을 고려해보세요:

// JoinSelect.css.ts
import { recipe } from '@vanilla-extract/recipes';

export const triggerWrapper = recipe({
  base: { /* 기본 스타일 */ },
  variants: {
    isOpen: {
      true: { /* 오픈 상태 스타일 */ },
    },
    isError: {
      true: { /* 에러 상태 스타일 */ },
    }
  }
});

그리고 컴포넌트에서는 다음과 같이 사용할 수 있습니다:

<Select.Trigger 
  className={triggerWrapper({ 
    isOpen, 
    isError 
  })} 
  aria-invalid={isError}
>

Also applies to: 31-32, 34-34

src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (1)

38-45: 유효성 검사 로직이 개선되었습니다.

불필요한 Boolean() 래퍼를 제거하여 코드가 더 간결해졌습니다. 추가적인 개선을 위해 조건들을 상수로 분리하는 것을 고려해보세요.

다음과 같이 리팩토링하면 가독성이 더욱 향상될 수 있습니다:

+ const isEmailsValid = !errors.contactEmail && !errors.univEmail;
+ const isAgreementsValid = serviceAgreeCheck.isTermOfService && serviceAgreeCheck.isPrivacy;
  const allValid =
    oauthEmail &&
    univEmail &&
-   !errors.contactEmail &&
-   !errors.univEmail &&
+   isEmailsValid &&
    isEmailVerified &&
-   serviceAgreeCheck.isTermOfService &&
-   serviceAgreeCheck.isPrivacy;
+   isAgreementsValid;
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx (1)

13-22: control prop의 타입 안전성을 개선할 수 있습니다.

현재 control prop이 any 타입으로 정의되어 있는데, react-hook-form의 구체적인 타입을 사용하면 더 안전한 타입 체크가 가능합니다.

다음과 같이 개선해 보세요:

- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- control: any;
+ import { Control } from 'react-hook-form';
+ control: Control<any>;
src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.css.ts (2)

6-13: 매직 넘버를 상수로 분리하는 것이 좋을 것 같습니다.

gap: '1.6rem'height: '30.8rem'와 같은 매직 넘버는 의미있는 상수로 분리하면 유지보수가 더 용이할 것 같습니다.

다음과 같이 상수를 분리하는 것을 제안드립니다:

+const LAYOUT_CONSTANTS = {
+  GAP: '1.6rem',
+  HEIGHT: '30.8rem',
+} as const;

 export const emptyPostCardLayout = style({
   display: 'flex',
   flexDirection: 'column',
   alignItems: 'center',
   justifyContent: 'center',
-  gap: '1.6rem',
-  height: '30.8rem',
+  gap: LAYOUT_CONSTANTS.GAP,
+  height: LAYOUT_CONSTANTS.HEIGHT,
 });

22-30: 텍스트 스타일을 더 효율적으로 구성할 수 있습니다.

emptyListTitleemptyListContent가 비슷한 구조를 가지고 있어 베이스 스타일을 공유할 수 있을 것 같습니다.

다음과 같이 리팩토링하는 것을 제안드립니다:

+const baseTextStyle = style({
+  color: colors.text04,
+});

 export const emptyListTitle = style([
+  baseTextStyle,
   {
     ...fonts.body.normal.M16,
-    color: colors.text04,
   },
 ]);

 export const emptyListContent = style([
+  baseTextStyle,
   {
     ...fonts.label.small.M12,
-    color: colors.text04,
   },
 ]);
src/app/home/components/PostCardList/PostCardList.css.ts (1)

1-7: 구현이 깔끔하고 명확합니다!

grid 레이아웃을 사용하여 카드 리스트를 구현한 방식이 적절합니다.

반응형 디자인 고려 제안

모바일 환경에서의 사용성을 위해 미디어 쿼리를 추가하는 것을 고려해보세요.

다음과 같이 반응형 그리드를 구현할 수 있습니다:

 export const postCardListLayout = style({
   display: 'grid',
-  gridTemplateColumns: '1fr 1fr 1fr',
+  gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
   gap: '1.2rem',
 });

이렇게 하면 화면 크기에 따라 자동으로 그리드 열 수가 조정됩니다.

src/app/home/components/PostContainer/AreaFilter/AreaFilter.css.ts (3)

8-22: CSS 변수를 활용한 동적 스타일링이 잘 구현되어 있습니다.

트리거 래퍼의 스타일링에서 색상을 CSS 변수로 처리한 것이 좋은 접근 방식입니다. 다만, CSS 변수의 기본값 설정을 고려해보시면 좋을 것 같습니다.

다음과 같이 기본값을 추가하는 것을 고려해보세요:

 export const triggerWrapper = style({
   ...fonts.label.large.SB14,
-  color: 'var(--trigger-color)',
-  backgroundColor: 'var(--trigger-bg)',
+  color: 'var(--trigger-color, ${colors.text06})',
+  backgroundColor: 'var(--trigger-bg, ${colors.field02})',
   display: 'flex',
   alignItems: 'center',

56-57: 스크롤 관련 스타일 속성을 개선할 수 있습니다.

overflow: 'scroll'은 항상 스크롤바를 표시합니다. 필요할 때만 스크롤바가 나타나도록 overflow: 'auto'를 사용하는 것이 더 나은 사용자 경험을 제공할 수 있습니다.

다음과 같이 수정하는 것을 추천드립니다:

-  overflow: 'scroll',
+  overflow: 'auto',

Also applies to: 133-134


191-212: 버튼 레시피의 재사용성이 좋습니다.

공통 스타일을 base로 정의하고 variant를 통해 타입별 스타일을 구분한 것이 좋은 패턴입니다. 다만, 버튼의 hover 상태에 대한 스타일이 누락되어 있습니다.

다음과 같이 hover 상태를 추가하는 것을 고려해보세요:

 variants: {
   type: {
     reset: {
       backgroundColor: colors.field03,
       color: colors.text06,
+      selectors: {
+        '&:hover': {
+          backgroundColor: colors.field04,
+        },
+      },
     },
     save: {
       backgroundColor: colors.primaryMint,
       color: colors.text01,
+      selectors: {
+        '&:hover': {
+          backgroundColor: colors.primaryMintHover,
+        },
+      },
     },
   },
 },
src/app/join/page.tsx (2)

30-38: 성공 단계에 대한 조건부 렌더링이 추가되었습니다.

성공 단계일 때의 레이아웃이 명확하게 분리되어 있습니다. 하지만 이 부분의 중복 코드를 줄일 수 있을 것 같습니다.

다음과 같이 공통 레이아웃을 추출하는 것을 고려해보세요:

+ const renderContent = () => (
+   role === ROLE.researcher ? <ResearcherForm /> : <ParticipantForm />
+ );

  if (step === STEP.success) {
    return (
      <section className={joinLayout}>
        <div className={contentContainer}>
-         {role === ROLE.researcher ? <ResearcherForm /> : <ParticipantForm />}
+         {renderContent()}
        </div>
      </section>
    );
  }

51-53: 진행 상태 표시줄의 동적 스타일링이 개선되었습니다.

assignInlineVars를 사용한 진행 상태 표시가 깔끔하게 구현되었습니다. 하지만 매직 넘버('50%', '100%')를 상수로 분리하는 것이 좋을 것 같습니다.

다음과 같이 상수를 분리하는 것을 추천드립니다:

+ const PROGRESS_WIDTH = {
+   [STEP.email]: '50%',
+   [STEP.info]: '100%',
+   [STEP.success]: '100%',
+ } as const;

  style={assignInlineVars({
-   '--progress-width': step === STEP.email ? '50%' : '100%',
+   '--progress-width': PROGRESS_WIDTH[step],
  })}
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.css.ts (2)

6-10: border-radius 값에 디자인 토큰 사용을 고려해보세요.

하드코딩된 '1rem' 대신 디자인 시스템의 토큰을 사용하면 일관성 있는 스타일링이 가능합니다.


16-32: 버튼 스타일의 개선 가능한 부분이 있습니다.

  1. 하드코딩된 값들을 디자인 토큰으로 대체하는 것이 좋습니다:

    • padding: '0.7rem 1.6rem'
    • borderRadius: '1rem'
    • right: '1.2rem'
    • top: '1rem'
  2. 절대 위치(absolute positioning)는 반응형 레이아웃에서 문제를 일으킬 수 있습니다. 더 유연한 레이아웃 방식을 고려해보세요.

src/app/join/components/JoinSuccessStep/JoinSuccessStep.css.ts (3)

6-13: 간격 값을 변수로 분리하는 것이 좋을 것 같습니다.

큰 간격 값(9rem)을 재사용 가능한 변수로 분리하면 일관성 있는 스페이싱 시스템을 구축하는 데 도움이 될 것 같습니다.

+const LAYOUT_GAP = '9rem';

 export const joinSuccessLayout = style({
   display: 'flex',
   flexDirection: 'column',
   alignItems: 'center',
   justifyContent: 'center',
-  gap: '9rem',
+  gap: LAYOUT_GAP,
   paddingTop: '3.6rem',
 });

28-30: 스타일 속성명을 더 명확하게 작성하면 좋을 것 같습니다.

수직 마진만 사용하는 경우 marginBlock을 사용하면 의도가 더 명확해질 것 같습니다.

 export const image = style({
-  margin: '3.2rem 0',
+  marginBlock: '3.2rem',
 });

32-39: 홈 링크에 호버 상태를 추가하면 좋을 것 같습니다.

사용자 경험 향상을 위해 호버 시 시각적 피드백을 제공하는 것이 좋습니다.

 export const homeLink = style({
   ...fonts.body.normal.SB16,
   backgroundColor: colors.primaryMint,
   color: colors.text01,
   borderRadius: '1.2rem',
   padding: '1.2rem 3.35rem',
   marginTop: '2.6rem',
+  transition: 'background-color 0.2s ease',
+  ':hover': {
+    backgroundColor: colors.primaryMintHover,
+  },
 });
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.css.ts (2)

6-10: 간격 값을 변수로 분리하는 것을 고려해보세요.

현재 구현은 잘 되어있지만, 간격 값('0.8rem')을 스타일 변수로 분리하면 재사용성과 일관성을 더욱 향상시킬 수 있습니다.

+import { spacing } from '@/styles/spacing';

 export const customRadioGroup = style({
   display: 'flex',
   flexFlow: 'row nowrap',
-  gap: '0.8rem',
+  gap: spacing.sm,
 });

12-30: 매직 넘버를 상수로 분리하면 좋겠습니다.

현재 구현은 잘 되어 있으며, 특히 접근성과 사용자 경험을 고려한 부분이 좋습니다. 다만 다음과 같은 개선사항을 제안드립니다:

  1. 높이('4.8rem'), 패딩('1rem 2rem'), 테두리 반경('1.2rem') 등의 값을 상수로 분리
  2. transition 속성에 대한 구체적인 속성 명시
+const RADIO_BUTTON = {
+  height: '4.8rem',
+  padding: '1rem 2rem',
+  borderRadius: '1.2rem',
+} as const;

 export const customRadioButton = style({
   ...fonts.label.large.M14,
   flexGrow: 1,
-  height: '4.8rem',
-  padding: '1rem 2rem',
+  height: RADIO_BUTTON.height,
+  padding: RADIO_BUTTON.padding,
   border: `0.1rem solid ${colors.line01}`,
-  borderRadius: '1.2rem',
+  borderRadius: RADIO_BUTTON.borderRadius,
   backgroundColor: colors.field01,
   cursor: 'pointer',
-  transition: 'all 0.2s ease',
+  transition: 'background-color 0.2s ease, border-color 0.2s ease',
   // ... rest of the code
src/app/join/components/JoinInput/JoinInput.css.ts (1)

19-40: 입력 필드 스타일링에 대한 개선 제안

현재 구현은 잘 되어 있으나, 다음과 같은 개선사항을 고려해보세요:

  1. borderRadius 값을 디자인 토큰으로 분리하면 일관성 유지에 도움이 될 것 같습니다.
  2. outline 스타일을 재사용 가능한 변수로 분리하는 것을 고려해보세요.
+import { radii } from '@/styles/tokens';
+
+const inputOutline = {
+  default: `0.1rem solid ${colors.line01}`,
+  focus: `0.1rem solid ${colors.lineTinted}`,
+  error: `0.1rem solid ${colors.textAlert}`,
+};
+
 export const joinInput = style({
   ...fonts.body.normal.M16,
   color: colors.text06,
-  border: `0.1rem solid ${colors.line01}`,
+  border: inputOutline.default,
   padding: '1.6rem',
-  borderRadius: '1.2rem',
+  borderRadius: radii.medium,
   selectors: {
     '&:focus': {
-      outline: `0.1rem solid ${colors.lineTinted}`,
+      outline: inputOutline.focus,
     },
     "&[aria-invalid='true']": {
-      outline: `0.1rem solid ${colors.textAlert}`,
+      outline: inputOutline.error,
     },
   },
 });
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx (1)

14-16: 접근성 개선을 위한 aria-label 추가 필요

툴팁 트리거 버튼에 스크린 리더 사용자를 위한 aria-label이 누락되어 있습니다.

-          <button>
+          <button aria-label="지역 선택에 대한 추가 정보">
            <Icon icon="Information" width={18} height={18} />
          </button>
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.css.ts (1)

6-9: 애니메이션 성능 최적화 제안

transform 속성에 GPU 가속을 활용하여 애니메이션 성능을 개선할 수 있습니다.

export const slideDownAndFade = keyframes({
-  from: { opacity: 0, transform: 'translateY(-2px)' },
-  to: { opacity: 1, transform: 'translateY(0)' },
+  from: { opacity: 0, transform: 'translate3d(0, -2px, 0)' },
+  to: { opacity: 1, transform: 'translate3d(0, 0, 0)' },
});
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.css.ts (1)

6-21: 버튼 스타일의 재사용성 개선 제안

버튼 스타일이 여러 곳에서 재사용될 수 있으므로, 공통 스타일을 분리하여 재사용 가능한 형태로 만드는 것이 좋습니다.

+ // common/styles/buttons.css.ts
+ export const baseButton = style({
+   borderRadius: '1.2rem',
+   padding: '1.2rem 0',
+   alignItems: 'center',
+ });
+
+ // JoinInfoStep.css.ts
export const joinButton = style([
+   baseButton,
  {
    ...fonts.body.normal.SB16,
    backgroundColor: colors.primaryMint,
    color: colors.text01,
-   borderRadius: '1.2rem',
-   padding: '1.2rem 0',
    width: '20rem',
-   alignItems: 'center',
    marginBottom: '5.6rem',
    selectors: {
      '&:disabled': {
        color: colors.text02,
        backgroundColor: colors.field04,
      },
    },
  }
]);
src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.css.ts (1)

46-65: 선택 항목 스타일의 유지보수성 개선 제안

상태별 스타일을 별도의 변수로 분리하면 유지보수가 더 용이해질 것 같습니다.

+ const itemStateStyles = {
+   highlighted: {
+     backgroundColor: colors.field02,
+     outline: 'none',
+   },
+   checked: {
+     backgroundColor: colors.primaryTinted,
+     color: colors.textPrimary,
+     border: `0.1rem solid ${colors.textPrimary}`,
+   },
+ };
+
export const selectItem = style({
  ...fonts.body.normal.M16,
  color: colors.text06,
  display: 'flex',
  padding: '0.6rem 1.2rem',
  alignItems: 'center',
  borderRadius: '1.2rem',
  cursor: 'pointer',
  selectors: {
-   '&[data-highlighted]': {
-     backgroundColor: colors.field02,
-     outline: 'none',
-   },
-   "&[data-state='checked']": {
-     backgroundColor: colors.primaryTinted,
-     color: colors.textPrimary,
-     border: `0.1rem solid ${colors.textPrimary}`,
-   },
+   '&[data-highlighted]': itemStateStyles.highlighted,
+   "&[data-state='checked']": itemStateStyles.checked,
  },
});
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (3)

19-19: 불필요한 주석 제거 필요

코드의 일관성을 위해 한글 주석을 제거하거나, 전체 코드베이스에서 주석 스타일을 통일하는 것이 좋습니다.

-import { joinContentContainer, joinForm } from '@/app/join/JoinPage.css'; // 이미 vanilla‐extract로 변환된 JoinPage 스타일 파일
+import { joinContentContainer, joinForm } from '@/app/join/JoinPage.css';

37-42: 폼 유효성 검사 로직 개선 제안

현재 구현된 isAllFilled 로직은 다음과 같은 개선이 가능합니다:

  1. 명시적인 타입 체크
  2. null 체크 로직 단순화
  3. 재사용 가능한 유틸리티 함수로 분리
-  const isAllFilled = values.every((value) => (value ?? '').trim() !== '' && value !== undefined);
+  const isValueFilled = (value: unknown): boolean => {
+    if (value === null || value === undefined) return false;
+    if (typeof value === 'string') return value.trim() !== '';
+    return true;
+  };
+  const isAllFilled = values.every(isValueFilled);

171-177: 접근성 개선 필요

제출 버튼의 접근성을 개선하기 위해 다음 사항들을 추가하는 것이 좋습니다:

  1. aria-label 속성
  2. 비활성화 상태일 때의 이유를 설명하는 aria-describedby
   <button
     className={joinButton}
     onClick={handleSubmit}
     disabled={!(isAllFilled && Object.keys(errors).length === 0)}
+    aria-label="회원가입 완료"
+    aria-describedby={!(isAllFilled && Object.keys(errors).length === 0) ? "submit-error" : undefined}
   >
     회원가입
   </button>
+  {!(isAllFilled && Object.keys(errors).length === 0) && (
+    <span id="submit-error" className="sr-only">
+      필수 항목을 모두 입력해주세요
+    </span>
+  )}
src/app/join/JoinPage.css.ts (3)

6-13: 패딩값을 디자인 토큰으로 관리하는 것을 고려해보세요.

레이아웃 스타일이 잘 구성되어 있습니다. 하지만 paddingTop: '8.4rem'과 같은 하드코딩된 값은 디자인 시스템의 일관성을 위해 spacing 토큰으로 관리하는 것이 좋습니다.


28-36: 간격 단위의 일관성을 유지하면 좋겠습니다.

스타일이 전반적으로 잘 구성되어 있으나, gap은 '2.8rem'을 사용하고 padding은 '3.2rem 4rem'을 사용하는 등 간격 단위가 일관적이지 않습니다. 디자인 시스템의 spacing 토큰을 활용하여 일관성을 유지하는 것이 좋겠습니다.


67-82: 버튼 스타일에 디자인 토큰 활용을 확대하면 좋겠습니다.

버튼 스타일이 전반적으로 잘 구성되어 있으나, 다음 값들을 디자인 토큰으로 관리하면 좋겠습니다:

  • padding: '1.2rem 0'
  • width: '20rem'
  • marginBottom: '5.6rem'

이렇게 하면 디자인 시스템의 일관성을 더욱 높일 수 있습니다.

src/app/join/components/JoinInput/JoinInput.tsx (2)

21-34: Props 유효성 검사 개선을 제안드립니다.

maxLength prop이 textarea에서 필수적으로 사용되지만, 현재는 선택적 prop으로 정의되어 있습니다. 타입 안정성을 높이기 위해 다음과 같은 개선을 제안드립니다:

-interface JoinInputProps<T extends FieldValues> {
+type BaseJoinInputProps<T extends FieldValues> = {
   type?: 'input' | 'textarea';
   name: Path<T>;
   control: Control<T>;
   rules?: object;
   placeholder?: string;
   label?: string;
   required?: boolean;
   disabled?: boolean;
   tip?: string;
   value?: PathValue<T, Path<T>>;
   isTip?: boolean;
-  maxLength?: number;
+}
+
+type JoinInputProps<T extends FieldValues> = 
+  | (BaseJoinInputProps<T> & { type?: 'input', maxLength?: number })
+  | (BaseJoinInputProps<T> & { type: 'textarea', maxLength: number });

128-139: UI/UX 개선을 제안드립니다.

에러 메시지와 팁 메시지의 가시성을 높이기 위해 다음과 같은 개선사항을 제안드립니다:

  1. 에러 메시지와 팁 메시지에 ID를 추가하여 aria-describedby와 연결:
-{fieldState.error && <span className={errorMessage}>{fieldState.error.message}</span>}
+{fieldState.error && (
+  <span className={errorMessage} id={`${name}-error`}>
+    {fieldState.error.message}
+  </span>
+)}

-{tip && !fieldState.error && (
-  <div className={tipWrapper}>
+{tip && !fieldState.error && (
+  <div className={tipWrapper} id={`${name}-tip`}>
  1. vanilla-extract에서 애니메이션 추가:
// JoinInput.css.ts
import { keyframes, style } from '@vanilla-extract/css';

const fadeIn = keyframes({
  from: { opacity: 0, transform: 'translateY(-5px)' },
  to: { opacity: 1, transform: 'translateY(0)' }
});

export const errorMessage = style({
  // ... 기존 스타일
  animation: `${fadeIn} 0.2s ease-in-out`
});
src/app/login/components/LoginCard/LoginCard.tsx (1)

50-55: 스타일 로직 개선을 제안드립니다.

역할에 따른 색상 매핑을 상수로 분리하면 코드의 가독성과 유지보수성이 향상될 것 같습니다.

다음과 같이 개선해보세요:

+const ROLE_STYLES = {
+  연구자: {
+    color: theme.colors.secondaryPink,
+    background: theme.colors.secondaryTinted,
+  },
+  참여자: {
+    color: theme.colors.primaryMint,
+    background: theme.colors.primaryTinted,
+  },
+} as const;

 style={assignInlineVars({
-  '--badge-color':
-    role === '연구자' ? theme.colors.secondaryPink : theme.colors.primaryMint,
-  '--badge-bg':
-    role === '연구자' ? theme.colors.secondaryTinted : theme.colors.primaryTinted,
+  '--badge-color': ROLE_STYLES[role].color,
+  '--badge-bg': ROLE_STYLES[role].background,
})}
src/app/login/LoginPage.css.ts (2)

9-9: 뷰포트 높이 계산 방식 개선이 필요합니다.

calc(100vh - 12.2rem)는 모바일 브라우저의 동적 툴바나 키보드가 표시될 때 문제가 될 수 있습니다.

다음과 같이 dvh 단위 사용을 고려해보세요:

-  height: 'calc(100vh - 12.2rem)',
+  minHeight: 'calc(100dvh - 12.2rem)',

20-23: 스타일 재사용성 향상이 필요합니다.

descriptionWrappersloganWrapper가 동일한 폰트 스타일과 색상을 사용하고 있습니다. 공통 스타일을 추출하여 재사용하면 유지보수가 더 쉬워질 것 같습니다.

다음과 같이 공통 스타일을 분리하는 것을 제안합니다:

const commonTextStyle = style({
  ...fonts.title.medium.SB20,
  color: colors.text06,
});

export const descriptionWrapper = style([
  commonTextStyle,
]);

export const sloganWrapper = style([
  commonTextStyle,
  {
    textAlign: 'center',
  }
]);
src/app/login/components/LoginCard/LoginCard.css.ts (1)

37-47: 로그인 버튼 스타일의 재사용성 개선이 필요합니다.

현재 loginButton 스타일이 너무 구체적으로 정의되어 있어 재사용이 어려울 수 있습니다. 기본 버튼 스타일과 변형을 분리하면 더 유연하게 사용할 수 있을 것 같습니다.

다음과 같이 스타일을 분리하는 것을 제안합니다:

export const baseButton = style({
  ...fonts.label.large.R14,
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  gap: '0.4rem',
  padding: '0.8rem',
  borderRadius: '0.8rem',
});

export const loginButton = style([
  baseButton,
  {
    backgroundColor: colors.field01,
    color: colors.text06,
  }
]);
src/app/join/components/EmailToast/EmailToast.css.ts (2)

7-19: z-index 관리 방식 개선 제안

theme.zIndex를 사용하여 z-index를 관리하는 것은 좋은 접근이지만, toast의 위치 지정에 있어 더 견고한 방식을 제안드립니다.

export const toastLayout = style({
  color: colors.text06,
  height: '5.2rem',
  backgroundColor: colors.fieldToast,
  boxShadow: '0px 4px 16px rgba(53, 59, 61, 0.2)',
  borderRadius: '8rem',
  padding: '1.4rem 2.4rem',
  position: 'fixed',
-  top: 0,
+  top: '2rem',
  left: '50%',
  transform: 'translateX(-50%)',
  zIndex: theme.zIndex.toastContent,
});

21-29: width 속성 최적화 제안

width: 'max-content' 대신 더 유연한 방식을 고려해보세요.

export const toastTitle = style({
  ...fonts.body.normal.SB16,
  color: colors.text01,
  display: 'flex',
  flexFlow: 'row nowrap',
  alignItems: 'center',
  gap: '1rem',
-  width: 'max-content',
+  minWidth: 'max-content',
+  maxWidth: '80vw',
});
src/app/home/components/Banner/Banner.css.ts (3)

10-15: 반응형 디자인 개선 제안

고정된 높이값(15vh)은 다양한 화면 크기에서 문제가 될 수 있습니다.

export const bannerWrapper = style({
  position: 'relative',
-  height: '15vh',
+  minHeight: '15vh',
+  maxHeight: '20rem',
  display: 'flex',
  alignItems: 'center',
});

27-30: 트랜지션 속성 구체화 제안

all 트랜지션은 성능에 영향을 줄 수 있습니다. 특정 속성만 트랜지션하는 것이 좋습니다.

export const carouselContainer = style({
  display: 'flex',
-  transition: 'all 1s',
+  transition: 'transform 1s ease-in-out',
});

37-40: 이미지 최적화 제안

이미지 렌더링 최적화를 위한 추가 속성을 제안드립니다.

export const bannerImage = style({
  width: '100%',
  height: 'auto',
+  objectFit: 'cover',
+  aspectRatio: '16 / 9',
});
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.css.ts (1)

6-24: 동적 스타일링을 위한 CSS 변수 사용을 고려해보세요.

현재 스타일이 정적으로 정의되어 있어 상태에 따른 스타일 변경이 어려울 수 있습니다.

체크박스의 상태(checked, disabled 등)에 따른 스타일링을 위해 CSS 변수 사용을 고려해보세요:

export const checkboxLayout = style({
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
+  backgroundColor: 'var(--checkbox-bg, transparent)',
+  opacity: 'var(--checkbox-opacity, 1)',
});
src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.css.ts (2)

8-10: 주석 처리된 코드는 제거해주세요.

불필요한 주석 처리된 코드가 남아있습니다. 버전 관리 시스템을 사용하고 있으므로 이전 코드는 히스토리에서 확인할 수 있습니다.

다음과 같이 주석을 제거해주세요:

export const triggerWrapper = style({
  ...fonts.label.large.SB14,
-  //   color: colors.text06,
  color: 'var(--trigger-color)',
  backgroundColor: 'var(--trigger-bg)',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'space-between',
  gap: '0.4rem',
  borderRadius: '1.2rem',
  padding: '0.5rem 1.4rem',
-  //   backgroundColor: colors.field01,
  cursor: 'pointer',
  ...
});

Also applies to: 17-18


21-22: 박스 쉐도우 값을 테마 상수로 분리하는 것이 좋습니다.

여러 곳에서 동일한 박스 쉐도우 값이 하드코딩되어 있습니다.

styles/theme.ts에 쉐도우 값을 상수로 정의하고 재사용하는 것을 추천드립니다:

// styles/theme.ts
export const shadows = {
  dropdown: '0px 4px 16px rgba(53, 59, 61, 0.2)',
  // ...other shadow values
};

Also applies to: 33-34

src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx (1)

36-36: 조건부 클래스네임 로직을 clsx나 classnames 라이브러리를 사용하여 개선할 수 있습니다.

템플릿 리터럴을 사용한 조건부 클래스네임 로직이 복잡해질 수 있습니다.

clsx나 classnames 라이브러리를 사용하여 더 깔끔하게 작성할 수 있습니다:

-<div className={`${checkboxLayout} ${isAllCheck ? allCheckWrapper : ''}`}>
+<div className={clsx(checkboxLayout, isAllCheck && allCheckWrapper)}>
src/app/home/components/Banner/Banner.tsx (1)

45-47: 이미지 최적화 개선이 필요합니다.

동일한 이미지를 여러 번 렌더링하는 것은 성능에 영향을 줄 수 있으며, 모든 이미지에 priority 속성을 사용하는 것은 최적화에 도움이 되지 않을 수 있습니다.

다음과 같이 개선해보세요:

-            <Image src={BannerImage} alt="배너1" className={bannerImage} priority />
-            <Image src={BannerImage} alt="배너2" className={bannerImage} priority />
-            <Image src={BannerImage} alt="배너3" className={bannerImage} priority />
+            <Image src={BannerImage} alt="배너1" className={bannerImage} priority />
+            <Image src={BannerImage} alt="배너2" className={bannerImage} loading="lazy" />
+            <Image src={BannerImage} alt="배너3" className={bannerImage} loading="lazy" />
src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.tsx (1)

47-48: TODO 주석에 대한 후속 조치가 필요합니다.

사용자 정보에서 gender와 age를 가져오는 기능이 아직 구현되지 않았습니다. 이 기능을 구현하여 사용자 경험을 개선할 수 있습니다.

이 기능을 구현하기 위한 코드를 생성해드릴까요?

src/app/home/components/PostContainer/AreaFilter/AreaFilter.tsx (1)

98-100: 성능 최적화가 필요합니다.

매 렌더링마다 reduce 연산이 실행되고 있습니다. 이 값을 메모이제이션하면 성능을 개선할 수 있습니다.

다음과 같이 수정을 제안합니다:

+ const totalCount = useMemo(() => 
+   postArea?.reduce((acc, cur) => acc + cur.count, 0) ?? 0,
+   [postArea]
+ );

// ...

- {postArea?.reduce((acc, cur) => acc + cur.count, 0)}
+ {totalCount}
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.css.ts (1)

6-21: 스타일 개선 제안

버튼 스타일링에 대해 다음과 같은 개선사항을 제안드립니다:

  1. 반응형 디자인을 위해 고정 너비 대신 상대적 단위 사용을 고려해보세요.
  2. 재사용성을 위해 마진을 props로 받아 유연하게 조절할 수 있도록 하는 것이 좋습니다.
export const nextButton = style({
  ...fonts.body.normal.SB16,
  backgroundColor: colors.primaryMint,
  color: colors.text01,
  borderRadius: '1.2rem',
  padding: '1.2rem 0',
-  width: '20rem',
+  width: 'min(20rem, 100%)',
-  marginBottom: '5.6rem',
  alignItems: 'center',
  selectors: {
    '&:disabled': {
      color: colors.text02,
      backgroundColor: colors.field04,
    },
  },
});
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.css.ts (1)

25-37: 디자인 토큰 활용 제안

버튼 스타일의 매직 넘버들을 디자인 토큰으로 분리하면 일관성 있는 디자인 시스템 구축에 도움이 될 것 같습니다.

+ // tokens.css.ts에 추가
+ export const spacing = {
+   sm: '0.7rem',
+   md: '1rem',
+   lg: '1.6rem',
+ };
+ export const borderRadius = {
+   md: '1rem',
+ };

// AuthCodeInput.css.ts
export const authCodeButton = style({
  ...fonts.label.large.SB14,
-  padding: '0.7rem 1.6rem',
-  borderRadius: '1rem',
+  padding: `${spacing.sm} ${spacing.lg}`,
+  borderRadius: borderRadius.md,
  backgroundColor: colors.primaryMint,
  color: colors.text01,
  selectors: {
    '&:disabled': {
      backgroundColor: colors.field04,
      color: colors.text02,
    },
  },
});
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (3)

50-50: TODO 주석 처리 필요

이미 인증된 유저 처리에 대한 TODO 주석이 있습니다. 이 부분을 구현하는데 도움이 필요하신가요?


79-80: 불필요한 불리언 표현식 단순화

aria-invalid 속성의 불리언 표현식을 단순화할 수 있습니다.

- aria-invalid={fieldState.invalid ? true : false}
+ aria-invalid={fieldState.invalid}
🧰 Tools
🪛 Biome (1.9.4)

[error] 80-80: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)


91-92: 에러 처리 개선 제안

필드 에러와 API 에러를 구분하여 표시하는 것이 좋습니다. 사용자가 어떤 종류의 에러가 발생했는지 명확하게 알 수 있도록 스타일을 다르게 적용하는 것을 추천드립니다.

// UnivAuthInput.css.ts에 추가
+ export const fieldError = style({
+   color: colors.error,
+   fontSize: '1.2rem'
+ });
+ 
+ export const apiError = style([
+   fieldError,
+   {
+     fontWeight: 'bold'
+   }
+ ]);

// UnivAuthInput.tsx
- {fieldState.error && <span className={errorMessage}>{fieldState.error.message}</span>}
- {sendError && <span className={errorMessage}>{sendError.message}</span>}
+ {fieldState.error && <span className={fieldError}>{fieldState.error.message}</span>}
+ {sendError && <span className={apiError}>{sendError.message}</span>}
src/app/home/components/PostContainer/PostContainer.css.ts (2)

18-23: 반응형 레이아웃 고려가 필요합니다.

고정된 최소 높이(40rem)는 다양한 화면 크기에서 문제를 일으킬 수 있습니다. 다음과 같은 개선을 고려해보세요:

export const postCardContainer = style({
  display: 'flex',
  flexDirection: 'column',
  gap: '0.6rem',
-  minHeight: '40rem',
+  minHeight: 'min(40rem, 90vh)',
});

30-34: 모바일 대응을 고려해보세요.

현재 필터 래퍼의 레이아웃이 모바일에서는 부적절할 수 있습니다. 화면 크기가 작을 때는 세로 배치로 전환하는 것을 고려해보세요.

export const filterWrapper = style({
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'space-between',
+  '@media': {
+    'screen and (max-width: 768px)': {
+      flexDirection: 'column',
+      alignItems: 'flex-start',
+      gap: '1rem',
+    },
+  },
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa90ab3 and 0ce1c19.

📒 Files selected for processing (82)
  • src/app/home/components/Banner/Banner.css.ts (1 hunks)
  • src/app/home/components/Banner/Banner.styles.ts (0 hunks)
  • src/app/home/components/Banner/Banner.tsx (2 hunks)
  • src/app/home/components/PostCard/PostCard.css.ts (1 hunks)
  • src/app/home/components/PostCard/PostCard.styles.ts (0 hunks)
  • src/app/home/components/PostCard/PostCard.tsx (3 hunks)
  • src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.css.ts (1 hunks)
  • src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.styles.ts (0 hunks)
  • src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.tsx (1 hunks)
  • src/app/home/components/PostCardList/PostCardList.css.ts (1 hunks)
  • src/app/home/components/PostCardList/PostCardList.styles.ts (0 hunks)
  • src/app/home/components/PostCardList/PostCardList.tsx (2 hunks)
  • src/app/home/components/PostContainer/AreaFilter/AreaFilter.css.ts (1 hunks)
  • src/app/home/components/PostContainer/AreaFilter/AreaFilter.styles.ts (0 hunks)
  • src/app/home/components/PostContainer/AreaFilter/AreaFilter.tsx (4 hunks)
  • src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.css.ts (1 hunks)
  • src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.styles.ts (0 hunks)
  • src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.tsx (2 hunks)
  • src/app/home/components/PostContainer/FilterContainer/FilterContainer.css.ts (1 hunks)
  • src/app/home/components/PostContainer/FilterContainer/FilterContainer.styles.ts (0 hunks)
  • src/app/home/components/PostContainer/FilterContainer/FilterContainer.tsx (2 hunks)
  • src/app/home/components/PostContainer/PostContainer.css.ts (1 hunks)
  • src/app/home/components/PostContainer/PostContainer.styles.ts (0 hunks)
  • src/app/home/components/PostContainer/PostContainer.tsx (3 hunks)
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.css.ts (1 hunks)
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.styles.ts (0 hunks)
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.tsx (2 hunks)
  • src/app/join/JoinPage.css.ts (1 hunks)
  • src/app/join/JoinPage.styles.ts (0 hunks)
  • src/app/join/components/EmailToast/EmailToast.css.ts (1 hunks)
  • src/app/join/components/EmailToast/EmailToast.styles.ts (0 hunks)
  • src/app/join/components/EmailToast/EmailToast.tsx (2 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.css.ts (1 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.styles.ts (0 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx (2 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.css.ts (1 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.styles.ts (0 hunks)
  • src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.tsx (2 hunks)
  • src/app/join/components/JoinInput/JoinInput.css.ts (1 hunks)
  • src/app/join/components/JoinInput/JoinInput.styles.ts (0 hunks)
  • src/app/join/components/JoinInput/JoinInput.tsx (6 hunks)
  • src/app/join/components/JoinSuccessStep/JoinSuccessStep.css.ts (1 hunks)
  • src/app/join/components/JoinSuccessStep/JoinSuccessStep.styles.ts (0 hunks)
  • src/app/join/components/JoinSuccessStep/JoinSuccessStep.tsx (2 hunks)
  • src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (3 hunks)
  • src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.css.ts (1 hunks)
  • src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx (2 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.css.ts (1 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (6 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.css.ts (1 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (2 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.css.ts (1 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (2 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.css.ts (1 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.styles.ts (0 hunks)
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx (4 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.css.ts (1 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.styles.ts (0 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (4 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.css.ts (1 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx (3 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.css.ts (1 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.styles.ts (0 hunks)
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (3 hunks)
  • src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.styles.ts (0 hunks)
  • src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (3 hunks)
  • src/app/join/page.tsx (3 hunks)
  • src/app/login/LoginPage.css.ts (1 hunks)
  • src/app/login/LoginPage.styles.ts (0 hunks)
  • src/app/login/components/LoginCard.styles.ts (0 hunks)
  • src/app/login/components/LoginCard/LoginCard.css.ts (1 hunks)
  • src/app/login/components/LoginCard/LoginCard.tsx (4 hunks)
  • src/app/login/google/GoogleLoginPage.css.ts (1 hunks)
  • src/app/login/google/GoogleLoginPage.styles.ts (0 hunks)
  • src/app/login/google/page.tsx (2 hunks)
  • src/app/login/layout.tsx (1 hunks)
  • src/app/login/naver/NaverLoginPage.css.ts (1 hunks)
⛔ Files not processed due to max files limit (5)
  • src/app/login/naver/NaverLoginPage.styles.ts
  • src/app/login/naver/page.tsx
  • src/app/login/page.tsx
  • src/app/page.tsx
  • src/components/Icon/icons/AllEmpty.tsx
💤 Files with no reviewable changes (27)
  • src/app/login/google/GoogleLoginPage.styles.ts
  • src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.styles.ts
  • src/app/home/components/PostContainer/FilterContainer/FilterContainer.styles.ts
  • src/app/home/components/PostCardList/PostCardList.styles.ts
  • src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.styles.ts
  • src/app/join/components/EmailToast/EmailToast.styles.ts
  • src/app/home/components/PostContainer/PostContainer.styles.ts
  • src/app/join/JoinPage.styles.ts
  • src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.styles.ts
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.styles.ts
  • src/app/login/LoginPage.styles.ts
  • src/app/login/components/LoginCard.styles.ts
  • src/app/join/components/JoinSuccessStep/JoinSuccessStep.styles.ts
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.styles.ts
  • src/app/home/components/PostCard/PostCard.styles.ts
  • src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.styles.ts
  • src/app/join/components/JoinInput/JoinInput.styles.ts
  • src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.styles.ts
  • src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.styles.ts
  • src/app/home/components/Banner/Banner.styles.ts
  • src/app/home/components/PostContainer/AreaFilter/AreaFilter.styles.ts
  • src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts
  • src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.styles.ts
  • src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.styles.ts
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts
  • src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.styles.ts
  • src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.styles.ts
✅ Files skipped from review due to trivial changes (4)
  • src/app/home/components/PostContainer/PostContainer.tsx
  • src/app/home/components/PostCard/PostCard.tsx
  • src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.css.ts
  • src/app/home/components/PostContainer/FilterContainer/FilterContainer.css.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx

[error] 80-80: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

🔇 Additional comments (60)
src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.tsx (1)

3-8: 스타일 임포트 경로가 올바르게 변경되었습니다.

Emotion에서 vanilla-extract로의 마이그레이션이 정상적으로 이루어졌습니다.

src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (1)

8-8: 스타일 임포트 마이그레이션이 올바르게 수행되었습니다.

Emotion에서 vanilla-extract로의 마이그레이션이 성공적으로 이루어졌습니다.

src/app/home/components/PostContainer/FilterContainer/FilterContainer.tsx (2)

1-1: 스타일 마이그레이션이 올바르게 수행되었습니다.

Emotion에서 vanilla-extract로의 마이그레이션이 성공적으로 이루어졌습니다:

  • .styles.ts에서 .css.ts로의 import 변경
  • css prop에서 className prop으로의 전환

컴포넌트의 기능적 로직은 그대로 유지되어 있습니다.

Also applies to: 12-12


6-8: 컴포넌트 구조가 잘 설계되어 있습니다.

  • TypeScript 인터페이스를 통한 명확한 props 타입 정의
  • 자식 컴포넌트들로의 일관된 props 전달
  • 필터 변경 핸들링이 적절하게 구현되어 있습니다

Also applies to: 10-18

src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.css.ts (1)

1-5: 임포트 구성이 깔끔합니다!

외부 라이브러리와 내부 모듈을 구분하여 임포트한 것이 좋습니다.

src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (2)

20-21: 폼 유효성 검사 로직이 잘 구현되었습니다.

useWatch를 사용하여 여러 필드의 값을 동시에 모니터링하고, every 메서드로 모든 필드가 채워졌는지 확인하는 방식이 효율적입니다.


24-25: Emotion에서 vanilla-extract로의 마이그레이션이 깔끔하게 이루어졌습니다.

css prop에서 className으로의 전환이 일관되게 적용되었으며, 기존 스타일링의 기능을 그대로 유지하고 있습니다.

Also applies to: 57-57

src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx (1)

6-16: 스타일 임포트가 올바르게 마이그레이션되었습니다.

Emotion에서 vanilla-extract로의 마이그레이션이 정상적으로 이루어졌습니다.

src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (3)

1-1: 'use client' 지시문이 적절히 추가되었습니다.

클라이언트 사이드 렌더링이 필요한 컴포넌트에 'use client' 지시문이 올바르게 추가되었습니다.


9-9: 스타일 임포트가 vanilla-extract로 성공적으로 마이그레이션되었습니다.

Emotion에서 vanilla-extract로의 스타일 마이그레이션이 올바르게 이루어졌습니다. 스타일 파일의 경로와 임포트 구문이 적절히 수정되었습니다.


27-31: allValid 로직이 개선되었습니다.

Boolean 래퍼를 제거하여 조건문이 더 간결해졌습니다. 하지만 이 로직에 대한 단위 테스트 추가를 고려해보시기 바랍니다.

유효성 검사 로직이 예상대로 동작하는지 확인하기 위한 테스트 케이스 작성을 추천드립니다. 테스트 코드 작성을 도와드릴까요?

src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (1)

6-6: 스타일 임포트가 잘 구성되어 있습니다!

스타일 변수명이 명확하고 계층 구조가 잘 표현되어 있습니다.

src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)

6-6: 스타일 임포트가 올바르게 마이그레이션되었습니다!

Emotion에서 vanilla-extract로의 스타일 임포트 전환이 깔끔하게 이루어졌습니다.

Also applies to: 12-12


52-53: 스타일 적용 방식이 성공적으로 마이그레이션되었습니다!

Emotion의 css prop에서 vanilla-extract의 className으로의 전환이 일관성 있게 이루어졌습니다.

Also applies to: 77-77

src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx (2)

1-11: 스타일 마이그레이션 및 클라이언트 컴포넌트 설정이 올바르게 되었습니다!

클라이언트 사이드 렌더링을 위한 'use client' 지시문 추가와 vanilla-extract로의 스타일 마이그레이션이 잘 이루어졌습니다.


24-62: 컴포넌트 구현이 잘 되어있습니다!

  • vanilla-extract로의 스타일 마이그레이션이 일관성 있게 잘 적용되었습니다.
  • react-hook-form과의 통합이 잘 유지되었습니다.
  • 필수 필드 표시와 에러 상태 처리가 적절합니다.
src/app/home/components/PostCardList/EmptyPostCard/EmptyPostCard.css.ts (2)

1-5: 스타일 임포트가 잘 구성되어 있습니다!

전역 스타일 토큰(colors, fonts)을 활용하여 일관된 디자인 시스템을 유지하고 있습니다.


15-20: 컨텐츠 스타일링이 깔끔하게 되어있습니다!

flex 레이아웃을 활용하여 컨텐츠를 효과적으로 배치하고 있습니다.

src/app/home/components/PostContainer/AreaFilter/AreaFilter.css.ts (3)

1-6: 라이브러리 임포트와 기본 설정이 잘 구성되어 있습니다.

필요한 vanilla-extract 라이브러리와 공통 스타일이 적절하게 임포트되어 있습니다.


77-104: 버튼 레시피가 잘 구현되어 있습니다.

선택 상태에 따른 스타일 변화가 명확하게 정의되어 있으며, 기본값 설정도 적절합니다.


156-160: 접근성 관련 스타일이 잘 구현되어 있습니다.

체크박스를 시각적으로 숨기면서도 스크린 리더 접근성을 유지한 방식이 좋습니다.

src/app/join/components/JoinSuccessStep/JoinSuccessStep.tsx (3)

5-11: 스타일 임포트 경로가 올바르게 변경되었습니다.

Emotion에서 vanilla-extract로의 마이그레이션이 잘 이루어졌습니다.


21-28: className 속성으로의 전환이 잘 이루어졌습니다.

기존 Emotion의 css prop에서 vanilla-extract의 className으로 전환이 깔끔하게 이루어졌습니다.


29-31: Link 컴포넌트의 스타일링이 적절하게 적용되었습니다.

Next.js의 Link 컴포넌트에 className을 사용한 스타일 적용이 올바르게 이루어졌습니다.

src/app/join/page.tsx (1)

3-3: 동적 CSS 변수 지원을 위한 적절한 도구가 추가되었습니다.

@vanilla-extract/dynamicassignInlineVars를 사용하여 동적 스타일링을 구현한 것이 좋습니다.

src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.css.ts (4)

1-5: 스타일 관련 import 문이 잘 구성되어 있습니다!

필요한 스타일 유틸리티와 프로젝트의 디자인 토큰을 적절히 가져오고 있습니다.


12-14: 필수 입력 필드 스타일이 잘 정의되어 있습니다!

디자인 시스템의 색상 토큰을 적절히 활용하고 있습니다.


34-38: 수정 버튼 스타일이 잘 정의되어 있습니다!

색상 토큰을 일관성 있게 사용하고 있습니다.


40-43: 에러 메시지 스타일이 적절히 정의되어 있습니다!

폰트와 색상 토큰을 올바르게 활용하고 있습니다.

src/app/join/components/JoinSuccessStep/JoinSuccessStep.css.ts (3)

1-4: 모듈 가져오기가 잘 구성되어 있습니다!

공유 스타일 모듈을 활용하여 일관된 디자인 시스템을 유지하고 있습니다.


15-21: 타이틀 컨테이너 스타일이 잘 구현되어 있습니다!

플렉스박스를 활용한 중앙 정렬과 일관된 간격 사용이 돋보입니다.


23-26: 타이포그래피와 색상 토큰 사용이 훌륭합니다!

공유 폰트 스타일과 시맨틱 컬러 토큰을 활용하여 디자인 시스템을 잘 따르고 있습니다.

src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.css.ts (3)

1-5: 스타일 임포트가 잘 구성되어 있습니다!

공유 스타일 토큰(colors, fonts)을 활용하여 일관된 디자인 시스템을 유지하고 있습니다.


32-41: 활성 상태 스타일링이 잘 구현되어 있습니다!

호버 상태에서도 일관된 스타일을 유지하여 사용자 경험이 자연스럽습니다.


43-45: 에러 상태 스타일링이 명확하게 구현되어 있습니다!

시맨틱 컬러 토큰을 사용하여 에러 상태를 시각적으로 잘 표현하고 있습니다.

src/app/join/components/JoinInput/JoinInput.css.ts (4)

1-5: 스타일 설정이 잘 구성되어 있습니다!

공유 스타일 토큰(colors, fonts)을 활용하여 일관된 디자인 시스템을 구현한 점이 좋습니다.


6-18: 컨테이너와 라벨 스타일링이 적절합니다!

Flexbox를 활용한 레이아웃과 일관된 간격 설정이 잘 되어있습니다.


42-56: 부가 UI 요소들의 스타일링이 적절합니다!

필수 입력 표시와 리셋 버튼의 위치 지정이 잘 구현되어 있습니다.


57-79: 피드백 텍스트 스타일링이 잘 구현되어 있습니다!

에러 메시지, 도움말, 글자 수 표시 등의 스타일링이 디자인 시스템을 잘 따르고 있습니다.

src/app/join/JoinPage.css.ts (6)

1-5: LGTM! 의존성 가져오기가 잘 구성되어 있습니다.

필요한 스타일 유틸리티와 디자인 토큰을 올바르게 가져오고 있습니다.


15-20: 컨테이너 스타일링이 깔끔하게 되어 있습니다.

flex 레이아웃을 사용한 컨테이너 구성이 적절합니다.


22-26: 제목 컨테이너의 레이아웃이 잘 구성되어 있습니다.

space-between과 center 정렬을 적절히 활용하고 있습니다.


38-41: 타이포그래피 스타일이 디자인 시스템을 잘 따르고 있습니다.

폰트와 색상에 디자인 토큰을 적절히 활용하고 있습니다.


43-56: 프로그레스 바 구현이 효율적으로 되어 있습니다.

CSS 변수를 활용한 동적 너비 조절과 부드러운 전환 효과가 잘 구현되어 있습니다.


58-65: 폼 레이아웃이 깔끔하게 구성되어 있습니다.

flex 레이아웃을 활용한 폼 요소 배치가 적절합니다.

src/app/join/components/JoinInput/JoinInput.tsx (1)

6-17: 스타일 임포트가 잘 구성되어 있습니다!

vanilla-extract로의 마이그레이션이 깔끔하게 이루어졌으며, 스타일 네이밍이 명확하고 일관성 있게 구성되어 있습니다.

src/app/login/components/LoginCard/LoginCard.tsx (1)

3-3: 스타일 정의 파일 검증이 필요합니다.

스타일 변수들이 올바르게 정의되어 있는지 확인해주세요.

다음 스크립트를 실행하여 스타일 정의를 확인할 수 있습니다:

Also applies to: 13-14

✅ Verification successful

스타일 정의 및 파일 검증 완료

  • LoginCard.css.tsLoginPage.css.ts 파일에서 필요한 모든 스타일 변수들이 올바르게 정의되어 있는 것으로 확인되었습니다.
  • 해당 스타일 변수들이 vanilla-extract 마이그레이션 과정에 문제없이 반영되고 있으므로, dynamic styling 및 관련 import 구문(assignInlineVars) 사용에 이상이 없습니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# LoginCard.css.ts와 LoginPage.css.ts 파일에서 스타일 변수 정의 확인
fd -e css.ts -x cat {} \; | grep -E "badge|buttonContainer|cardTitleContainer|loginButton|loginCardLayout|descriptionWrapper"

Length of output: 659

src/app/login/google/GoogleLoginPage.css.ts (1)

3-5: 이전 파일과 동일한 이슈가 있습니다.

NaverLoginPage.css.ts 파일에서 언급한 것과 같이, 매직 넘버에 대한 설명이 필요하며 중복된 스타일을 공통 모듈로 분리해야 합니다.

src/app/login/layout.tsx (1)

1-1: 구현이 깔끔하고 vanilla-extract 마이그레이션이 잘 되었습니다.

Emotion의 css prop에서 vanilla-extract의 className으로의 전환이 올바르게 이루어졌습니다.

Also applies to: 4-4

src/app/login/google/page.tsx (1)

9-27: 구글 로그인 구현이 안전하고 효율적으로 되어 있습니다.

다음과 같은 좋은 구현 사례들이 있습니다:

  1. useRef를 사용하여 중복 제출 방지
  2. URL 파라미터의 안전한 처리
  3. vanilla-extract로의 스타일 마이그레이션이 깔끔하게 완료
src/app/home/components/PostCardList/PostCardList.tsx (1)

3-3: 스타일 마이그레이션이 올바르게 수행되었습니다!

Emotion에서 vanilla-extract로의 마이그레이션이 정확하게 이루어졌습니다:

  • .styles.css 임포트 변경
  • css prop → className prop 변경

Also applies to: 21-21

src/app/join/components/EmailToast/EmailToast.tsx (1)

5-5: 스타일 마이그레이션이 올바르게 수행되었습니다!

Emotion에서 vanilla-extract로의 마이그레이션이 정확하게 이루어졌습니다:

  • .styles.css 임포트 변경
  • css prop → className prop 변경

Also applies to: 20-20, 25-25, 30-30

src/app/home/components/PostCard/PostCard.css.ts (1)

1-89: 구현이 잘 되었습니다!

vanilla-extract로의 마이그레이션이 잘 이루어졌으며, 테마 변수와 폰트 스타일을 적절히 활용하고 있습니다.

src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.css.ts (1)

1-126: 접근성과 사용성을 고려한 훌륭한 구현입니다!

CSS 변수를 활용한 동적 스타일링, 트랜지션 효과, 그리고 포커스 상태 처리가 잘 구현되어 있습니다.

src/app/home/components/PostContainer/ContactTargetPopover/ContactTargetFilter.tsx (1)

88-91: 동적 스타일링 구현이 잘 되었습니다.

assignInlineVars를 사용한 동적 스타일링 구현이 깔끔하게 되었습니다. 선택 상태에 따른 색상 변경이 명확하게 처리되었습니다.

src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.css.ts (1)

11-18: 타이머 위치 지정 방식 개선 필요

절대 위치(absolute positioning) 사용은 다른 컨텍스트에서 문제를 일으킬 수 있습니다. 상대적인 위치 지정을 고려해보세요.

export const authTimerWrapper = style({
-  position: 'absolute',
-  right: '1.2rem',
-  top: '1rem',
+  position: 'relative',
  display: 'flex',
  alignItems: 'center',
  gap: '0.8rem',
+  marginLeft: 'auto',
});
✅ Verification successful

타이머 스타일 개선 검증 완료

현재 src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.css.ts 파일의 authTimerWrapper에서는 절대 위치(absolute positioning)가 사용되고 있습니다. 이는 부모 컨테이너가 상대 위치(relative)를 가지고 있지 않은 경우, 다양한 레이아웃 이슈를 발생시킬 수 있습니다. 리뷰어가 제시한 diff처럼, 절대 위치 대신 상대 위치를 사용하고 오른쪽 정렬을 위해 marginLeft: 'auto'를 적용하는 방식이 더 유연한 레이아웃 구성을 제공할 수 있습니다.

  • 파일: src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.css.ts
  • 이슈: authTimerWrapperposition: 'absolute'가 사용됨으로써, 다른 컨텍스트에서 레이아웃 충돌 가능성이 존재합니다.
  • 개선 제안: 제시된 diff와 같이 position: 'relative'로 변경하고 marginLeft: 'auto'를 추가하여 위치 지정 방식을 개선할 수 있습니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts or web queries found

src/app/home/components/PostContainer/PostContainer.css.ts (4)

1-5: 스타일 설정이 잘 구성되어 있습니다!

공유 스타일 토큰(colors, fonts)을 활용하여 일관된 디자인 시스템을 구현한 점이 좋습니다.


13-16: 타이틀 스타일링이 잘 구현되어 있습니다!

공유 폰트 스타일과 색상 토큰을 적절히 활용하였습니다.


25-28: 게시물 수 스타일링이 잘 구현되어 있습니다!

레이블 폰트와 텍스트 색상이 적절히 적용되었습니다.


6-11: rem 단위 사용이 디자인 시스템과 일치하는지 확인해주세요.

현재 margin과 gap에 사용된 rem 값들(2rem, 1.6rem)이 디자인 시스템의 spacing 토큰과 일치하는지 확인이 필요합니다.

@@ -95,7 +96,7 @@ const JoinInfoStep = ({ handleSubmit }: JoinInfoStepProps) => {
onChange={(value) => setValue('basicAddressInfo.region', value)}
placeholder="시·도"
options={JOIN_REGION}
isError={Boolean(fieldState.error) && Boolean(!field.value)}
isError={Boolean(fieldState.error) && !field.value}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 상태 체크 로직 통일 필요

Select 컴포넌트들의 에러 상태 체크 로직이 일관되지 않습니다:

  • 기본 지역: Boolean(fieldState.error) && !field.value
  • 추가 지역: Boolean(fieldState.error)

에러 처리 로직을 통일하여 일관성을 유지하는 것이 좋습니다.

-  isError={Boolean(fieldState.error) && !field.value}
+  isError={Boolean(fieldState.error)}

Also applies to: 113-113, 136-136

Comment on lines +3 to +5
export const emptyLayout = style({
height: 'calc(100vh - 17.8rem)',
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

매직 넘버에 대한 설명이 필요하며, 중복된 스타일을 공통 모듈로 분리해야 합니다.

17.8rem이라는 매직 넘버의 의미와 계산 방식에 대한 주석이 필요합니다. 또한 이 스타일이 GoogleLoginPage.css.ts에도 동일하게 존재하므로, 공통 모듈로 분리하는 것이 좋습니다.

다음과 같이 공통 모듈로 분리하는 것을 제안합니다:

// src/app/login/styles/common.css.ts
+import { style } from '@vanilla-extract/css';
+
+/**
+ * 전체 높이에서 헤더 높이(17.8rem)를 뺀 레이아웃
+ */
+export const emptyLayout = style({
+  height: 'calc(100vh - 17.8rem)',
+});

그리고 각 페이지에서 임포트하여 사용:

-import { style } from '@vanilla-extract/css';
+import { emptyLayout } from '../styles/common.css';
-
-export const emptyLayout = style({
-  height: 'calc(100vh - 17.8rem)',
-});
+export { emptyLayout };
📝 Committable suggestion

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

Suggested change
export const emptyLayout = style({
height: 'calc(100vh - 17.8rem)',
});
import { style } from '@vanilla-extract/css';
/**
* 전체 높이에서 헤더 높이(17.8rem) 레이아웃
*/
export const emptyLayout = style({
height: 'calc(100vh - 17.8rem)',
});
Suggested change
export const emptyLayout = style({
height: 'calc(100vh - 17.8rem)',
});
import { emptyLayout } from '../styles/common.css';
export { emptyLayout };

Comment on lines +8 to +9
minWidth: '100rem',
height: 'calc(100vh - 12.2rem)',
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

반응형 디자인 개선이 필요합니다.

최소 너비를 100rem으로 고정하면 작은 화면에서 가로 스크롤이 발생할 수 있습니다. 모바일 환경을 고려하여 반응형으로 구현하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

-  minWidth: '100rem',
+  width: '100%',
+  maxWidth: '100rem',
📝 Committable suggestion

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

Suggested change
minWidth: '100rem',
height: 'calc(100vh - 12.2rem)',
width: '100%',
maxWidth: '100rem',
height: 'calc(100vh - 12.2rem)',

Comment on lines +27 to +28
color: 'var(--badge-color)',
backgroundColor: 'var(--badge-bg)',
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

CSS 변수에 대한 타입 안전성이 필요합니다.

--badge-color--badge-bg CSS 변수가 타입 체크 없이 사용되고 있습니다. vanilla-extract의 createVar()를 사용하면 타입 안전성을 보장할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

import { createVar, style } from '@vanilla-extract/css';

const badgeColor = createVar();
const badgeBg = createVar();

export const badge = style({
  ...fonts.label.large.SB14,
  width: 'fit-content',
  padding: '0.4rem 0.8rem',
  borderRadius: '1.2rem',
  color: badgeColor,
  backgroundColor: badgeBg,
});

// 사용 예시:
// assignInlineVars({
//   [badgeColor]: colors.text01,
//   [badgeBg]: colors.field03,
// })

Comment on lines +24 to +28
export const checkbox = style({
position: 'absolute',
opacity: 0,
pointerEvents: 'none',
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선이 필요한 hidden checkbox 구현입니다.

opacity: 0pointerEvents: 'none'만으로는 스크린 리더 사용자를 위한 충분한 접근성을 제공하지 않을 수 있습니다.

다음과 같이 수정하는 것을 추천드립니다:

export const checkbox = style({
  position: 'absolute',
-  opacity: 0,
-  pointerEvents: 'none',
+  clip: 'rect(0 0 0 0)',
+  clipPath: 'inset(50%)',
+  height: '1px',
+  overflow: 'hidden',
+  position: 'absolute',
+  whiteSpace: 'nowrap',
+  width: '1px'
});
📝 Committable suggestion

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

Suggested change
export const checkbox = style({
position: 'absolute',
opacity: 0,
pointerEvents: 'none',
});
export const checkbox = style({
position: 'absolute',
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
overflow: 'hidden',
position: 'absolute',
whiteSpace: 'nowrap',
width: '1px'
});

@@ -18,7 +18,7 @@ const JoinCheckboxContainer = ({
const isAllCheck = isTermOfService && isPrivacy && isAdvertise && (isRecommend ?? true);

return (
<div css={termContainer}>
<div className={termContainer}>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

체크박스 그룹에 대한 시맨틱 마크업이 필요합니다.

현재 일반 div로 감싸져 있는 체크박스 그룹을 시맨틱하게 개선할 수 있습니다.

다음과 같이 fieldset과 legend를 사용하여 체크박스 그룹을 시맨틱하게 마크업해주세요:

-<div className={termContainer}>
+<fieldset className={termContainer}>
+  <legend className="sr-only">서비스 이용약관 동의</legend>
   {/* checkbox components */}
-</div>
+</fieldset>

추가로 screen reader only 클래스도 정의해주세요:

// styles/utils.css.ts
export const srOnly = style({
  position: 'absolute',
  width: '1px',
  height: '1px',
  padding: '0',
  margin: '-1px',
  overflow: 'hidden',
  clip: 'rect(0,0,0,0)',
  border: '0'
});

Comment on lines +117 to +121
<input
className={ageInput}
onChange={handleChangeFilteredAge}
placeholder="만 나이 입력"
/>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력값 유효성 검사가 필요합니다.

나이 입력 필드에 대한 유효성 검사가 누락되었습니다. 음수나 문자 입력을 방지하고 적절한 나이 범위를 설정하는 것이 좋습니다.

다음과 같이 수정을 제안합니다:

 <input
   className={ageInput}
   onChange={handleChangeFilteredAge}
   placeholder="만 나이 입력"
+  type="number"
+  min="1"
+  max="120"
+  value={filteredAge}
 />
📝 Committable suggestion

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

Suggested change
<input
className={ageInput}
onChange={handleChangeFilteredAge}
placeholder="만 나이 입력"
/>
<input
className={ageInput}
onChange={handleChangeFilteredAge}
placeholder="만 나이 입력"
type="number"
min="1"
max="120"
value={filteredAge}
/>

Comment on lines 134 to 139
<input
type="checkbox"
css={checkbox}
className={checkbox}
checked={!!checkedSubAreas[subArea.name]}
onChange={() => handleSubAreaCheck(subArea.name)}
/>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선이 필요합니다.

체크박스 입력에 대한 접근성 레이블이 누락되었습니다.

다음과 같이 수정을 제안합니다:

 <input
   type="checkbox"
   className={checkbox}
   checked={!!checkedSubAreas[subArea.name]}
   onChange={() => handleSubAreaCheck(subArea.name)}
+  aria-label={`${subAreaMapper[subArea.name]} 선택`}
 />
📝 Committable suggestion

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

Suggested change
<input
type="checkbox"
css={checkbox}
className={checkbox}
checked={!!checkedSubAreas[subArea.name]}
onChange={() => handleSubAreaCheck(subArea.name)}
/>
<input
type="checkbox"
className={checkbox}
checked={!!checkedSubAreas[subArea.name]}
onChange={() => handleSubAreaCheck(subArea.name)}
aria-label={`${subAreaMapper[subArea.name]} 선택`}
/>

<div className={univInputWrapper}>
<input
{...field}
style={{ width: '100%' }}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인라인 스타일 제거 필요

인라인 스타일을 CSS 모듈로 이동하여 스타일 일관성을 유지하는 것이 좋습니다.

// UnivAuthInput.css.ts에 추가
+ export const fullWidthInput = style({
+   width: '100%'
+ });

// UnivAuthInput.tsx
- <input style={{ width: '100%' }} />
+ <input className={fullWidthInput} />
📝 Committable suggestion

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

Suggested change
style={{ width: '100%' }}
import { style } from '@vanilla-extract/css';
export const fullWidthInput = style({
width: '100%',
});
Suggested change
style={{ width: '100%' }}
// ... other imports
import { fullWidthInput } from './UnivAuthInput.css';
// ... inside the component's render method
<input className={fullWidthInput} />


// 체크박스 숨김 처리
export const checkbox = style({
position: 'absolute',
Copy link
Member

Choose a reason for hiding this comment

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

#43 이 PR에 a11yhidden 클래스를 전역으로 생성해두었는데 해당 클래스를 사용해도 괜찮을 것 같습니다!

Copy link
Member

@eeeyooon eeeyooon left a comment

Choose a reason for hiding this comment

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

마이그레이션 작업 고생 많으셨습니다! 🤝
일정이 바쁜 와중 시간을 많이 쓰셨다니..ㅠㅠ 다시 한번 고생하셨어요!
해당 pr 머지 후 따로 이슈를 생성해서 emotion 관련 설정 및 파일들은 모두 제거하겠습니다!

Copy link

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

🧹 Nitpick comments (3)
src/app/join/JoinPage.css.ts (2)

6-12: 반응형 디자인 고려 필요

minHeight 계산에서 고정된 픽셀값을 사용하고 있습니다. 다양한 화면 크기에 대응하기 위해 반응형 단위 사용을 고려해보세요.

-  width: '56rem',
+  width: 'min(56rem, 100%)',
-  minHeight: 'calc(100vh - 12.2rem)',
+  minHeight: 'calc(100vh - var(--header-height, 12.2rem))',

58-64: 프로그레스 바 애니메이션 최적화

현재 transition 속성이 width에만 적용되어 있습니다. 성능 최적화를 위해 transform을 사용하는 것이 좋습니다.

export const progressBarFill = style({
-  width: 'var(--progress-width)',
+  transform: 'scaleX(var(--progress-scale, 0))',
+  transformOrigin: 'left',
   height: '100%',
   backgroundColor: colors.primaryMint,
   borderRadius: '0.6rem',
-  transition: 'width 1s',
+  transition: 'transform 1s ease-out',
});
src/types/post.ts (1)

10-10: 타입 변경 사유에 대한 문서화 제안

univName이 null을 허용하게 된 배경과 사유를 코드 주석으로 문서화하면 좋을 것 같습니다.

다음과 같이 주석을 추가하는 것을 제안드립니다:

+  /** 대학교 이름이 없는 경우를 처리하기 위해 null 허용 */
  univName: string | null;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce1c19 and 03a5b8d.

📒 Files selected for processing (6)
  • src/app/home/components/PostCard/PostCard.tsx (3 hunks)
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.tsx (3 hunks)
  • src/app/join/JoinPage.css.ts (1 hunks)
  • src/app/join/layout.tsx (1 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/types/post.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/home/components/PostCard/PostCard.tsx
  • src/app/home/components/PostContainer/ProgressMethodFilter/ProgressMethodFilter.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/layout.tsx

[error] 32-32: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (4)
src/app/layout.tsx (1)

2-2: 스타일 마이그레이션과 무관한 변경사항입니다.

이 PR의 주요 목적은 Emotion에서 vanilla-extract로의 스타일 마이그레이션입니다. Beusable RUM 클라이언트 추가는 별도의 PR로 분리하는 것이 좋을 것 같습니다.

src/app/join/layout.tsx (1)

3-8: 스타일 마이그레이션이 잘 수행되었습니다!

Emotion에서 vanilla-extract로의 마이그레이션이 깔끔하게 이루어졌으며, 컴포넌트의 구조가 잘 유지되었습니다.

src/app/join/JoinPage.css.ts (1)

1-5: 디자인 토큰 활용이 잘 되었습니다!

colors와 fonts 토큰을 일관성 있게 사용하여 디자인 시스템을 잘 반영하였습니다.

src/types/post.ts (1)

13-14: 타입 사용의 일관성이 잘 유지되었습니다

durationInfo 내의 startDateendDate처럼 다른 nullable 필드들과 동일한 패턴으로 타입이 정의되어 있어 일관성이 잘 유지되었습니다.

Comment on lines +27 to +45
<Script
id="beusable-script"
dangerouslySetInnerHTML={{
__html: `
(function(w, d, a){
w.__beusablerumclient__ = {
load: function(src){
var b = d.createElement("script");
b.src = src;
b.async = true;
b.type = "text/javascript";
d.getElementsByTagName("head")[0].appendChild(b);
}
};
w.__beusablerumclient__.load(a + "?url=" + encodeURIComponent(d.URL));
})(window, document, "//rum.beusable.net/load/b250203e183750u380");
`,
}}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

보안 위험: dangerouslySetInnerHTML 사용을 재고해 주세요.

dangerouslySetInnerHTML을 사용하면 XSS(Cross-Site Scripting) 공격에 취약할 수 있습니다. Beusable RUM 클라이언트 스크립트를 더 안전한 방식으로 구현하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안드립니다:

-        <Script
-          id="beusable-script"
-          dangerouslySetInnerHTML={{
-            __html: `
-            (function(w, d, a){
-                w.__beusablerumclient__ = {
-                    load: function(src){
-                        var b = d.createElement("script");
-                        b.src = src; 
-                        b.async = true; 
-                        b.type = "text/javascript";
-                        d.getElementsByTagName("head")[0].appendChild(b);
-                    }
-                };
-                w.__beusablerumclient__.load(a + "?url=" + encodeURIComponent(d.URL));
-            })(window, document, "//rum.beusable.net/load/b250203e183750u380");
-          `,
-          }}
-        />
+        <Script
+          id="beusable-script"
+          src="//rum.beusable.net/load/b250203e183750u380"
+          strategy="afterInteractive"
+        />
📝 Committable suggestion

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

Suggested change
<Script
id="beusable-script"
dangerouslySetInnerHTML={{
__html: `
(function(w, d, a){
w.__beusablerumclient__ = {
load: function(src){
var b = d.createElement("script");
b.src = src;
b.async = true;
b.type = "text/javascript";
d.getElementsByTagName("head")[0].appendChild(b);
}
};
w.__beusablerumclient__.load(a + "?url=" + encodeURIComponent(d.URL));
})(window, document, "//rum.beusable.net/load/b250203e183750u380");
`,
}}
/>
<Script
id="beusable-script"
src="//rum.beusable.net/load/b250203e183750u380"
strategy="afterInteractive"
/>
🧰 Tools
🪛 Biome (1.9.4)

[error] 32-32: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

Comment on lines +75 to +90
export const nextButton = style({
...fonts.body.normal.SB16,
backgroundColor: colors.primaryMint,
color: colors.text01,
borderRadius: '1.2rem',
padding: '1.2rem 0',
width: '20rem',
alignItems: 'center',
marginBottom: '5.6rem',
selectors: {
'&:disabled': {
color: colors.text02,
backgroundColor: colors.field04,
},
},
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선 필요

버튼 스타일에 focus와 hover 상태에 대한 스타일이 누락되어 있습니다. 접근성 향상을 위해 이러한 상태들도 고려해야 합니다.

export const nextButton = style({
  ...fonts.body.normal.SB16,
  backgroundColor: colors.primaryMint,
  color: colors.text01,
  borderRadius: '1.2rem',
  padding: '1.2rem 0',
  width: '20rem',
  alignItems: 'center',
  marginBottom: '5.6rem',
  selectors: {
    '&:disabled': {
      color: colors.text02,
      backgroundColor: colors.field04,
    },
+   '&:hover:not(:disabled)': {
+     backgroundColor: colors.primaryMintHover,
+   },
+   '&:focus-visible': {
+     outline: `2px solid ${colors.primaryMint}`,
+     outlineOffset: '2px',
+   },
  },
});
📝 Committable suggestion

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

Suggested change
export const nextButton = style({
...fonts.body.normal.SB16,
backgroundColor: colors.primaryMint,
color: colors.text01,
borderRadius: '1.2rem',
padding: '1.2rem 0',
width: '20rem',
alignItems: 'center',
marginBottom: '5.6rem',
selectors: {
'&:disabled': {
color: colors.text02,
backgroundColor: colors.field04,
},
},
});
export const nextButton = style({
...fonts.body.normal.SB16,
backgroundColor: colors.primaryMint,
color: colors.text01,
borderRadius: '1.2rem',
padding: '1.2rem 0',
width: '20rem',
alignItems: 'center',
marginBottom: '5.6rem',
selectors: {
'&:disabled': {
color: colors.text02,
backgroundColor: colors.field04,
},
'&:hover:not(:disabled)': {
backgroundColor: colors.primaryMintHover,
},
'&:focus-visible': {
outline: `2px solid ${colors.primaryMint}`,
outlineOffset: '2px',
},
},
});

@@ -7,7 +7,7 @@ export interface PostInfo {
experimentPostId: number;
title: string;
views: number;
univName: string;
univName: string | null;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

univName 타입 변경에 따른 영향도 확인 필요

univName 필드의 타입이 string에서 string | null로 변경되었습니다. 이는 breaking change이므로 다음 사항들을 확인해주세요:

  1. 이 인터페이스를 사용하는 모든 컴포넌트에서 null 처리가 적절히 되어있는지 확인
  2. 특히 PostCard와 PostCardList 컴포넌트에서 univName이 null일 때의 UI 처리 방식 검토

다음 스크립트를 실행하여 PostInfo 인터페이스를 사용하는 모든 위치를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of PostInfo interface and univName property

echo "Finding PostInfo interface usages:"
rg "PostInfo" -t typescript

echo -e "\nFinding direct univName property access:"
rg "univName" -t typescript

Length of output: 301


🏁 Script executed:

#!/bin/bash
# Updated: Find all usages of PostInfo interface and univName property by searching TypeScript files without the -t flag

echo "Finding PostInfo interface usages:"
rg "PostInfo" --glob "*.{ts,tsx}" || echo "No results for PostInfo"

echo -e "\nFinding direct univName property access:"
rg "univName" --glob "*.{ts,tsx}" || echo "No results for univName"

Length of output: 3148

univName 타입 변경 관련 추가 점검 필요

  • PostInfo 인터페이스 변경:
    src/types/post.ts에서 univName 필드를 string에서 string | null로 변경한 점은 null 값에 대한 처리를 명시적으로 하려는 의도로 보입니다.

  • 확인된 컴포넌트:
    PostCard 등 일부 컴포넌트에서는 univName 사용 시 null인 경우 기본값("비대면") 처리를 하고 있어 적절한 처리가 이루어지고 있습니다.

  • 이슈 발견:

    • src/apis/login.ts: univName이 여전히 string으로 선언되어 있어, PostInfo와 일관되지 않은 타입 정의가 확인되었습니다.
    • src/upload/hooks/useUploadExperimentPostMutation.ts: univName의 타입이 파일 내에서 univName?: string | null;univName: string;로 상이하게 선언되어 있어 타입 불일치로 인한 문제 발생 가능성이 있습니다.

전체 코드베이스에서 univName의 타입 적용이 일관적이고, null 값에 따른 예외 처리가 누락되지 않도록 위 파일들을 중심으로 추가 점검 후 수정이 필요합니다.

@rbgksqkr rbgksqkr merged commit 03cb649 into develop Feb 5, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design css와 같은 스타일링 refactor 코드 리팩토링
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[YS-243] 홈화면 & 로그인 & 회원가입 스타일링 vanilla-extract로 마이그레이션
2 participants