Skip to content

Feat/search#8

Merged
minsxo merged 6 commits into
mainfrom
feat/search
Jul 8, 2026
Merged

Feat/search#8
minsxo merged 6 commits into
mainfrom
feat/search

Conversation

@GirimNam

@GirimNam GirimNam commented Jul 8, 2026

Copy link
Copy Markdown
Member

📌 작업 내용

  • 선행기술 탐색 페이지(구성요소 분해) 컴포넌트 생성
  • 선행기술 탐색 페이지(구성요소 분해) 페이지 구현
  • 선행기술 탐색 페이지(구성요소 분해) 디자인 수정사항 반영
  • 사이드바 트랜지션 수정

✅ 변경 사항

  • 새로운 기능 추가
  • 버그 수정
  • 코드 개선 및 수정
  • 문서 작성 및 업데이트
  • 배포 관련 수정
  • 브랜치 관련 수정
  • 기타:

🔗 관련 이슈

Related to #7

🧪 체크리스트

  • 제가 작성한 코드를 리뷰했습니다.
  • 문서에 변경 사항을 반영했습니다.
  • 새로운 경고를 생성하지 않습니다.
  • 새로운 기능이나 수정 사항이 기존 테스트와 충돌하지 않음을 확인했습니다.

📸 스크린샷

image image image image

💬 리뷰 요청 사항

  • 사이드바 모션에 대해 검토 부탁드려요.

기타

Summary by CodeRabbit

  • New Features

    • 검색/발명 정보 화면이 실제 UI로 구현되어, 발명 정보·출원인 정보·구성요소 분석을 단계별로 입력하고 확인할 수 있게 됐습니다.
    • 특허 번호로 기술을 불러오는 모달과 AI 자동 생성 버튼, 결과 개수 조절 버튼이 추가됐습니다.
    • 체크리스트와 구성요소 목록 입력 UI가 새로 제공됩니다.
  • UI Improvements

    • 사이드바, 상단바, 레이아웃 스크롤 동작이 더 자연스럽게 개선됐습니다.
    • 입력창, 텍스트영역, 버튼 스타일과 접근성이 정리됐습니다.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Too big: expected string to have <=250 characters at "tone_instructions"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • 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
📝 Walkthrough

Walkthrough

선행기술 탐색 페이지(/search)가 실제 UI/상태 로직으로 구현되고, 구성요소 목록, 체크리스트, 특허번호 불러오기 모달, 결과개수 버튼 등 다수의 신규 컴포넌트가 추가되었습니다. 아울러 레이아웃 스크롤 처리, 사이드바 전환 방식, 체크박스 스타일, 탑바 경로 매핑도 함께 조정되었습니다.

Changes

검색 페이지 기능 구현

Layer / File(s) Summary
검색 페이지 상태 및 이벤트 로직
src/app/(main)/search/page.tsx
elements/isLoading/isModalOpen 상태, 추가·삭제·변경 핸들러, AI 생성 시 2초 후 정적 데이터로 교체, 섹션 레이아웃 및 모달 열림/닫힘 배선을 구현합니다.
구성요소 목록 UI
src/components/search/ElementList/*
Element 타입, ElementList/ElementRow/ElementTitle/LoadingIndicator 컴포넌트를 추가해 로딩 표시, 행 렌더링, 텍스트영역 자동 높이 조절을 구현합니다.
체크리스트/푸터/결과개수 버튼
src/components/search/Checklist.tsx, src/components/search/Footer.tsx, src/components/search/ResultCountButton.tsx
고정 항목 체크리스트, 결과 개수 증감(최소 1) 버튼, 탐색 시작 disabled 버튼을 포함한 Footer를 추가합니다.
특허 불러오기 모달 및 단계 타이틀
src/components/search/PatentImportModal.tsx, src/components/search/SearchTitle.tsx
배경 클릭 닫기와 특허번호 입력·제출을 처리하는 모달, 단계번호/제목/라벨을 표시하는 Title 컴포넌트를 추가합니다.
공용 UI 컴포넌트 확장 및 색상 토큰
src/components/ui/AI_Creation_Button.tsx, src/components/ui/TextArea.tsx, src/components/ui/TextField.tsx, src/app/globals.css
AICreationButton/TextArea를 신규 추가하고 TextField에 cva 기반 labelSize/gap 변형을 적용하며, ElementList 배경 및 아이콘 강조 색상 토큰을 추가합니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

레이아웃/사이드바/스타일 정리

Layer / File(s) Summary
레이아웃 스크롤/오버플로우 조정
src/app/(main)/layout.tsx, src/app/layout.tsx
main 요소에 scrollbar-hide를 추가하고 body 클래스를 h-full overflow-hidden으로 변경합니다.
사이드바 열림/닫힘 전환 방식 변경
src/components/sidebar/Sidebar.tsx, src/components/sidebar/SidebarNavItem.tsx
조건부 렌더링 대신 항상 렌더링 후 opacity/pointer-events/폭 전환으로 열림·닫힘 상태를 표현하도록 변경합니다.
체크박스 스타일 및 탑바 경로 매핑 수정
src/components/searchlist/Checkbox.tsx, src/components/topbar/Topbar.tsx
체크박스 테두리/색상 클래스를 icon-primary 계열로 교체하고, PAGE_LABELS 경로 키를 /history에서 /myhistory로 변경합니다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SearchPage
  participant ElementList
  participant PatentImportModal

  User->>SearchPage: AI 자동 생성 버튼 클릭
  SearchPage->>SearchPage: isLoading = true
  SearchPage->>ElementList: isLoading 전달 (로딩 표시)
  SearchPage-->>SearchPage: 2초 후 elements 교체, isLoading = false
  SearchPage->>ElementList: 갱신된 elements 전달 (행 렌더링)
  User->>SearchPage: 특허번호로 불러오기 버튼 클릭
  SearchPage->>PatentImportModal: isModalOpen = true (모달 표시)
  User->>PatentImportModal: 특허번호 입력 후 제출/닫기
  PatentImportModal-->>SearchPage: onSubmit/onClose 호출
  SearchPage->>SearchPage: isModalOpen = false (모달 닫힘)
Loading

Possibly related issues

Poem

토끼가 콩콩 뛰며 페이지를 짓네 🐰
구성요소 하나씩 상자에 담고
모달 문 열었다 닫았다 콩!
사이드바도 살랑살랑 접었다 펴고
스크롤바는 살짝 숨겨두었지
오늘도 코드밭에서 신나게 깡총! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive 제목이 'Feat/search'로만 되어 있어 변경 내용을 충분히 설명하지 못합니다. 선행기술 탐색 페이지 추가, 구성요소 분해 UI 구현 등 핵심 변경을 드러내는 구체적인 제목으로 바꾸세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search

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.

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

🧹 Nitpick comments (4)
src/components/ui/AI_Creation_Button.tsx (1)

7-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

forwardRef 대신 ref-as-prop 패턴 사용을 권장합니다.

React 19에서는 ref가 일반 prop으로 전달되므로 forwardRef가 더 이상 필요하지 않습니다. 같은 PR의 TextFieldTextArea는 이미 ref-as-prop 패턴을 사용하고 있어 일관성을 맞추는 것이 좋습니다.

♻️ 제안하는 리팩토링
-import { forwardRef } from "react";
 import { cn } from "`@/lib/cn`";
 import AICreationIcon from "`@/components/icons/icon-aiCreation.svg`";

-export type AICreationButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
+export type AICreationButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

-export const AICreationButton = forwardRef<HTMLButtonElement, AICreationButtonProps>(
-  ({ className, type = "button", ...props }, ref) => (
-    <button
-      ref={ref}
-      type={type}
-      className={cn(
-        "inline-flex cursor-pointer items-center gap-1 rounded-sm border border-stroke-primary bg-bg-surface py-2 pl-2 pr-3 text-label-15 text-primary-default transition-colors hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle",
-        className
-      )}
-      {...props}
-    >
-      <AICreationIcon className="h-5 w-5 [&_path]:fill-icon-primary-emphasize" aria-hidden />
-      AI 자동 생성
-    </button>
-  )
-);
-
-AICreationButton.displayName = "AICreationButton";
+export function AICreationButton({
+  className,
+  type = "button",
+  ref,
+  ...props
+}: AICreationButtonProps) {
+  return (
+    <button
+      ref={ref}
+      type={type}
+      className={cn(
+        "inline-flex cursor-pointer items-center gap-1 rounded-sm border border-stroke-primary bg-bg-surface py-2 pl-2 pr-3 text-label-15 text-primary-default transition-colors hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle",
+        className
+      )}
+      {...props}
+    >
+      <AICreationIcon className="h-5 w-5 [&_path]:fill-icon-primary-emphasize" aria-hidden />
+      AI 자동 생성
+    </button>
+  );
+}
🤖 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 `@src/components/ui/AI_Creation_Button.tsx` around lines 7 - 22, The
AICreationButton component is still using forwardRef, while this PR already uses
the React 19 ref-as-prop pattern in TextField and TextArea. Update
AICreationButton to accept ref as a normal prop on AICreationButtonProps, remove
the forwardRef wrapper, and keep the existing button behavior and styling
unchanged so the API stays consistent with the other components.
src/components/ui/TextField.tsx (1)

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

wrapperVariantsgap 변형이 TextArea와 일관성이 없습니다.

TextFieldwrapperVariantsgap을 동적 제어하지만, TextArea는 고정 gap-1.5를 사용합니다. 두 컴포넌트의 구조가 거의 동일하므로, TextArea에도 gap 변형을 추가하거나 공통 wrapper 패턴을 공유하는 것이 좋습니다.

🤖 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 `@src/components/ui/TextField.tsx` around lines 16 - 24, TextArea의 wrapper 간격이
TextField의 wrapperVariants와 달라 구조가 일관되지 않습니다. TextField의 wrapperVariants처럼
TextArea의 래퍼도 gap을 변형으로 받을 수 있게 하거나, 두 컴포넌트가 같은 wrapper 패턴을 공유하도록 정리해 주세요.
TextField, TextArea, wrapperVariants 식별자를 기준으로 찾아서 TextArea의 고정 gap-1.5를 동적 gap
변형으로 맞추면 됩니다.
src/components/searchlist/Checkbox.tsx (1)

53-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

filled 상태의 rounded-[1px]가 중복됩니다.

기본 클래스(라인 53)에 이미 rounded-[1px]가 포함되어 있으므로, isFilled 분기(라인 55)에서 다시 지정할 필요가 없습니다. twMerge가 중복을 제거하므로 기능적 문제는 없지만 코드가 불필요하게 중복됩니다.

♻️ 제안: 중복된 rounded-[1px] 제거
             isFilled
-              ? "border-icon-primary-default bg-icon-primary-default rounded-[1px]"
+              ? "border-icon-primary-default bg-icon-primary-default"
               : "border-outline-selected bg-bg-surface"
🤖 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 `@src/components/searchlist/Checkbox.tsx` around lines 53 - 55, The `Checkbox`
component has a duplicated `rounded-[1px]` class in the `isFilled` branch, since
the base class string already includes it. Update `Checkbox.tsx` in the class
composition logic so the filled-state branch only adds the classes that differ
from the default, keeping the shared `rounded-[1px]` on the base `twMerge`
input.
src/components/sidebar/Sidebar.tsx (1)

65-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

PreviousSearchItemopen prop이 전달되지 않아 일관성이 떨어집니다.

SidebarNavItem은 label을 항상 렌더링하도록 변경되었으나, PreviousSearchItem은 내부적으로 여전히 {open && ...} 조건부 렌더링을 사용합니다. 현재는 open이 전달되지 않아 기본값 true로 항상 렌더링되므로 기능적 문제는 없습니다. 하지만 향후 open={open}을 전달하면 애니메이션 없이 label이 사라지는 불일치가 발생할 수 있습니다.

♻️ 제안: PreviousSearchItem에 open prop 전달
-        <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." />
+        <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." open={open} />
🤖 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 `@src/components/sidebar/Sidebar.tsx` around lines 65 - 71,
`PreviousSearchItem` is still relying on its internal open-state behavior, which
is inconsistent with `SidebarNavItem` and can cause mismatched label rendering
later. Update the `Sidebar` usage to pass the current `open` state into
`PreviousSearchItem`, and verify the `PreviousSearchItem` component’s `open`
prop handling matches the intended always-rendering label behavior used
elsewhere. Keep the change localized to the `Sidebar` and `PreviousSearchItem`
symbols so the sidebar items behave consistently.
🤖 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 `@src/app/`(main)/search/page.tsx:
- Around line 144-149: The Search page’s PatentImportModal onSubmit handler is
ignoring the submitted patentNumber, so the value is lost. Update the inline
onSubmit passed from search/page.tsx to accept the modal payload from
PatentImportModal and either use the patentNumber for the next step or leave a
TODO if API wiring is not ready; keep the existing onClose behavior while
ensuring the submit callback does not discard its argument.
- Around line 144-149: `PatentImportModal`의 접근성 설정이 부족하므로, 모달 컨테이너를
`role="dialog"`와 `aria-modal="true"`를 갖는 진짜 다이얼로그로 보강하고 열린 동안 배경으로 포커스가 이동하지 않도록
포커스 트랩을 추가하세요. 또한 모달이 열릴 때 첫 포커스를 적절한 요소에 두고, Escape 키 입력으로 `onClose`가 호출되도록 키보드
처리도 넣어야 합니다. 구현 위치는 `PatentImportModal` 컴포넌트와 이를 여는 `search/page.tsx`의
`isModalOpen` 렌더링 흐름을 함께 확인해 수정하세요.
- Around line 144-149: `PatentImportModal` is receiving an `initialPatentNumber`
prop from the search page, but the component ignores it and always starts from
an empty state. Update `PatentImportModal` to either destructure and use
`initialPatentNumber` when initializing its local state, or remove the prop
entirely from both the modal usage in `Page` and the modal props/interface if it
is not needed. Make sure the fix is applied consistently in the
`PatentImportModal` component and its caller so the prop contract matches the
actual behavior.
- Around line 33-60: `handleAICreate` schedules a delayed state update with
`setTimeout`, but there is no cleanup when the search page component unmounts.
Update the `page.tsx` component to track the timer ID (for example with
`useRef`) and clear it in a `useEffect` cleanup, or otherwise ensure the timeout
is canceled before unmount so `setElements` and `setIsLoading` are never called
on an unmounted component.

In `@src/components/search/ElementList/ElementRow.tsx`:
- Around line 25-35: The textarea auto-resize logic in ElementRow currently only
runs inside the onChange handler, so it won’t update when description changes
from outside the input, such as after handleAICreate or on initial mount. Move
the height adjustment into ElementRow using a ref on the textarea plus a
useEffect that runs whenever description changes, and reuse the same
scrollHeight-based sizing there so the inline height is recalculated after
remounts and external value updates.

In `@src/components/search/PatentImportModal.tsx`:
- Around line 8-15: The PatentImportModal props include initialPatentNumber, but
the component ignores it and always starts patentNumber with an empty string.
Update PatentImportModal to either use initialPatentNumber when initializing the
useState in PatentImportModal or remove initialPatentNumber from
PatentImportModalProps if it is not needed; make sure the destructuring matches
the intended behavior.
- Line 45: The PatentImportModal submit action currently calls onSubmit from the
Button click even when patentNumber is empty, so add a guard in the
PatentImportModal component before invoking onSubmit and/or disable the Button
until a non-empty patentNumber is entered. Use the existing patentNumber state
and the Button onClick handler to prevent submitting blank values, and keep the
fix localized to PatentImportModal so the submit path is only reachable with a
valid patent number.
- Around line 17-47: The modal in PatentImportModal is missing dialog
accessibility and keyboard support. Update the top-level overlay/dialog
container to behave as a proper dialog by adding role="dialog",
aria-modal="true", and aria-labelledby tied to the heading in the modal header,
and ensure the close behavior handles Escape as well as click/close button
actions. Also add focus management so focus is trapped within the modal while
open and restored on close, using the modal container and the existing
onClose/onSubmit handlers to locate the changes.

In `@src/components/search/SearchTitle.tsx`:
- Around line 14-17: The SearchTitle component is always rendering the label <p>
even when the label prop is undefined, which leaves an empty DOM node and extra
spacing from the flex gap. Update SearchTitle so the label element is
conditionally rendered only when label has a value, keeping the title row layout
intact and removing the unnecessary empty <p>.

In `@src/components/ui/AI_Creation_Button.tsx`:
- Line 3: The import in AI_Creation_Button is failing because the SVG module
path cannot be resolved by Turbopack. Check that the icon file actually exists
under the icons directory and that the filename and casing exactly match the
AICreationIcon import path; if the asset was renamed or moved, update the import
to the correct existing SVG path or restore the missing file so the build can
resolve it.

In `@src/components/ui/TextArea.tsx`:
- Around line 5-14: `labelVariants` is duplicated between `TextArea` and
`TextField`; extract the shared `cva` definition into a common reusable module
and import it from both components. Move the existing `labelVariants`
configuration (including the `labelSize` variants and default) into a single
shared symbol, then update `TextArea` and `TextField` to reference that shared
export so styling stays consistent and future changes only happen in one place.

---

Nitpick comments:
In `@src/components/searchlist/Checkbox.tsx`:
- Around line 53-55: The `Checkbox` component has a duplicated `rounded-[1px]`
class in the `isFilled` branch, since the base class string already includes it.
Update `Checkbox.tsx` in the class composition logic so the filled-state branch
only adds the classes that differ from the default, keeping the shared
`rounded-[1px]` on the base `twMerge` input.

In `@src/components/sidebar/Sidebar.tsx`:
- Around line 65-71: `PreviousSearchItem` is still relying on its internal
open-state behavior, which is inconsistent with `SidebarNavItem` and can cause
mismatched label rendering later. Update the `Sidebar` usage to pass the current
`open` state into `PreviousSearchItem`, and verify the `PreviousSearchItem`
component’s `open` prop handling matches the intended always-rendering label
behavior used elsewhere. Keep the change localized to the `Sidebar` and
`PreviousSearchItem` symbols so the sidebar items behave consistently.

In `@src/components/ui/AI_Creation_Button.tsx`:
- Around line 7-22: The AICreationButton component is still using forwardRef,
while this PR already uses the React 19 ref-as-prop pattern in TextField and
TextArea. Update AICreationButton to accept ref as a normal prop on
AICreationButtonProps, remove the forwardRef wrapper, and keep the existing
button behavior and styling unchanged so the API stays consistent with the other
components.

In `@src/components/ui/TextField.tsx`:
- Around line 16-24: TextArea의 wrapper 간격이 TextField의 wrapperVariants와 달라 구조가
일관되지 않습니다. TextField의 wrapperVariants처럼 TextArea의 래퍼도 gap을 변형으로 받을 수 있게 하거나, 두
컴포넌트가 같은 wrapper 패턴을 공유하도록 정리해 주세요. TextField, TextArea, wrapperVariants 식별자를
기준으로 찾아서 TextArea의 고정 gap-1.5를 동적 gap 변형으로 맞추면 됩니다.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 61226f27-b21f-4814-92b1-9aa452e299f8

📥 Commits

Reviewing files that changed from the base of the PR and between e563fe1 and cfb4d18.

📒 Files selected for processing (20)
  • src/app/(main)/layout.tsx
  • src/app/(main)/search/page.tsx
  • src/app/globals.css
  • src/app/layout.tsx
  • src/components/search/Checklist.tsx
  • src/components/search/ElementList/ElementList.tsx
  • src/components/search/ElementList/ElementRow.tsx
  • src/components/search/ElementList/ElementTitle.tsx
  • src/components/search/ElementList/LoadingIndicator.tsx
  • src/components/search/Footer.tsx
  • src/components/search/PatentImportModal.tsx
  • src/components/search/ResultCountButton.tsx
  • src/components/search/SearchTitle.tsx
  • src/components/searchlist/Checkbox.tsx
  • src/components/sidebar/Sidebar.tsx
  • src/components/sidebar/SidebarNavItem.tsx
  • src/components/topbar/Topbar.tsx
  • src/components/ui/AI_Creation_Button.tsx
  • src/components/ui/TextArea.tsx
  • src/components/ui/TextField.tsx

Comment on lines +33 to +60
const handleAICreate = () => {
setIsLoading(true);
setTimeout(() => {
setElements([
{
id: crypto.randomUUID(),
name: "생분해성 베이스 수지",
description: "PLA·PBAT 블렌드 폴리에스터",
},
{
id: crypto.randomUUID(),
name: "표면개질 나노 충전제",
description: "실란 처리된 무기 나노입자",
},
{
id: crypto.randomUUID(),
name: "무용제 수계 분산 공정",
description: "유기용제 없이 수계 분산으로 코팅층 형성",
},
{
id: crypto.randomUUID(),
name: "UV 경화 기교",
description: "자외선 경화형 가교제에 의한 표면 가교",
},
]);
setIsLoading(false);
}, 2000);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

handleAICreatesetTimeout이 컴포넌트 언마운트 시 정리되지 않습니다.

컴포넌트가 2초 지연 중 언마운트되면 setElements/setIsLoading이 언마운트된 컴포넌트에서 호출됩니다. useEffect 정리 함수에서 타이머를 취소하거나, useRef로 타이머 ID를 추적하여 정리하는 것이 좋습니다.

♻️ 제안: useEffect 기반 정리
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
  const handleAICreate = () => {
    setIsLoading(true);
-   setTimeout(() => {
+   timerRef.current = setTimeout(() => {
      setElements([...]);
      setIsLoading(false);
    }, 2000);
  };
+
+ useEffect(() => {
+   return () => {
+     if (timerRef.current) clearTimeout(timerRef.current);
+   };
+ }, []);
📝 Committable suggestion

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

Suggested change
const handleAICreate = () => {
setIsLoading(true);
setTimeout(() => {
setElements([
{
id: crypto.randomUUID(),
name: "생분해성 베이스 수지",
description: "PLA·PBAT 블렌드 폴리에스터",
},
{
id: crypto.randomUUID(),
name: "표면개질 나노 충전제",
description: "실란 처리된 무기 나노입자",
},
{
id: crypto.randomUUID(),
name: "무용제 수계 분산 공정",
description: "유기용제 없이 수계 분산으로 코팅층 형성",
},
{
id: crypto.randomUUID(),
name: "UV 경화 기교",
description: "자외선 경화형 가교제에 의한 표면 가교",
},
]);
setIsLoading(false);
}, 2000);
};
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleAICreate = () => {
setIsLoading(true);
timerRef.current = setTimeout(() => {
setElements([
{
id: crypto.randomUUID(),
name: "생분해성 베이스 수지",
description: "PLA·PBAT 블렌드 폴리에스터",
},
{
id: crypto.randomUUID(),
name: "표면개질 나노 충전제",
description: "실란 처리된 무기 나노입자",
},
{
id: crypto.randomUUID(),
name: "무용제 수계 분산 공정",
description: "유기용제 없이 수계 분산으로 코팅층 형성",
},
{
id: crypto.randomUUID(),
name: "UV 경화 기교",
description: "자외선 경화형 가교제에 의한 표면 가교",
},
]);
setIsLoading(false);
}, 2000);
};
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
🤖 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 `@src/app/`(main)/search/page.tsx around lines 33 - 60, `handleAICreate`
schedules a delayed state update with `setTimeout`, but there is no cleanup when
the search page component unmounts. Update the `page.tsx` component to track the
timer ID (for example with `useRef`) and clear it in a `useEffect` cleanup, or
otherwise ensure the timeout is canceled before unmount so `setElements` and
`setIsLoading` are never called on an unmounted component.

Comment on lines +144 to +149
{isModalOpen && (
<PatentImportModal
initialPatentNumber=""
onClose={() => setIsModalOpen(false)}
onSubmit={() => setIsModalOpen(false)}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

onSubmit 콜백이 특허번호 데이터를 무시합니다.

PatentImportModalonSubmit({ patentNumber })를 호출하지만, 페이지의 onSubmit 핸들러는 인자를 받지 않고 모달만 닫습니다. 입력된 특허번호가 유실됩니다. API 연동 전이라면 TODO를 남기고, 아니면 patentNumber를 활용하도록 수정해야 합니다.

🤖 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, The Search page’s
PatentImportModal onSubmit handler is ignoring the submitted patentNumber, so
the value is lost. Update the inline onSubmit passed from search/page.tsx to
accept the modal payload from PatentImportModal and either use the patentNumber
for the next step or leave a TODO if API wiring is not ready; keep the existing
onClose behavior while ensuring the submit callback does not discard its
argument.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

모달 접근성: role, aria-modal, Escape 키, 포커스 트랩이 누락되었습니다.

PatentImportModalrole="dialog" / aria-modal="true" 없이 <div>로만 구현되어 있고, Escape 키로 닫기 및 포커스 트랩이 없습니다. 스크린 리더 사용자와 키보드 사용자가 모달을 벗어날 수 없거나 모달이 열려도 인지하지 못할 수 있습니다.

🤖 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, `PatentImportModal`의
접근성 설정이 부족하므로, 모달 컨테이너를 `role="dialog"`와 `aria-modal="true"`를 갖는 진짜 다이얼로그로 보강하고
열린 동안 배경으로 포커스가 이동하지 않도록 포커스 트랩을 추가하세요. 또한 모달이 열릴 때 첫 포커스를 적절한 요소에 두고, Escape 키
입력으로 `onClose`가 호출되도록 키보드 처리도 넣어야 합니다. 구현 위치는 `PatentImportModal` 컴포넌트와 이를 여는
`search/page.tsx`의 `isModalOpen` 렌더링 흐름을 함께 확인해 수정하세요.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

PatentImportModalinitialPatentNumber prop을 전달하지만 컴포넌트가 이를 사용하지 않습니다.

PatentImportModal의 인터페이스에는 initialPatentNumber가 선언되어 있으나, 실제 컴포넌트는 이를 destructuring조차 하지 않고 useState("")로 초기화합니다. 불필요한 prop을 제거하거나, 의도대로 초기값으로 사용하도록 수정해야 합니다.

♻️ 제안: initialPatentNumber 사용 또는 제거
- export function PatentImportModal({ onClose, onSubmit }: PatentImportModalProps) {
-   const [patentNumber, setPatentNumber] = useState("");
+ export function PatentImportModal({ initialPatentNumber = "", onClose, onSubmit }: PatentImportModalProps) {
+   const [patentNumber, setPatentNumber] = useState(initialPatentNumber);
🤖 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, `PatentImportModal`
is receiving an `initialPatentNumber` prop from the search page, but the
component ignores it and always starts from an empty state. Update
`PatentImportModal` to either destructure and use `initialPatentNumber` when
initializing its local state, or remove the prop entirely from both the modal
usage in `Page` and the modal props/interface if it is not needed. Make sure the
fix is applied consistently in the `PatentImportModal` component and its caller
so the prop contract matches the actual behavior.

Comment on lines +25 to +35
<textarea
value={description}
onChange={(e) => {
onChangeDescription(e.target.value);
e.target.style.height = "auto";
e.target.style.height = `${e.target.scrollHeight}px`;
}}
placeholder="구성요소 설명"
rows={1}
className="w-full resize-none overflow-hidden bg-transparent text-label-17 text-body-secondary placeholder:text-caption-label placeholder:text-label-17 focus:outline-none"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

textarea 자동 높이 조정이 외부 값 변경 시 동작하지 않습니다.

onChange 핸들러 내에서만 scrollHeight 기반 높이 조정이 수행됩니다. AI 생성(handleAICreate)으로 인해 새 element가 교체되거나 초기 렌더 시, textarea가 새로 마운트되면 인라인 height가 설정되지 않아 rows={1} 높이로 고정됩니다. 긴 설명 텍스트가 overflow-hidden으로 잘려 보일 수 있습니다.

🔧 제안하는 수정: ref와 useEffect로 마운트/값 변경 시 높이 조정
+import { useRef, useEffect } from "react";
 import TrashIcon from "`@/components/icons/icon-trashcan.svg`";
 import { ElementTitle } from "./ElementTitle";

 interface ElementRowProps {
   index: number;
   name: string;
   description: string;
   onDelete: () => void;
   onChangeName: (value: string) => void;
   onChangeDescription: (value: string) => void;
 }

 export function ElementRow({
   index,
   name,
   description,
   onDelete,
   onChangeName,
   onChangeDescription,
 }: ElementRowProps) {
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+  useEffect(() => {
+    const el = textareaRef.current;
+    if (el) {
+      el.style.height = "auto";
+      el.style.height = `${el.scrollHeight}px`;
+    }
+  }, [description]);
+
   return (
     <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-4 px-4 py-3">
       <ElementTitle index={index} value={name} onChange={onChangeName} />

       <textarea
+        ref={textareaRef}
         value={description}
         onChange={(e) => {
           onChangeDescription(e.target.value);
-          e.target.style.height = "auto";
-          e.target.style.height = `${e.target.scrollHeight}px`;
+          const el = textareaRef.current;
+          if (el) {
+            el.style.height = "auto";
+            el.style.height = `${el.scrollHeight}px`;
+          }
         }}
         placeholder="구성요소 설명"
         rows={1}
         className="w-full resize-none overflow-hidden bg-transparent text-label-17 text-body-secondary placeholder:text-caption-label placeholder:text-label-17 focus:outline-none"
       />
🤖 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 `@src/components/search/ElementList/ElementRow.tsx` around lines 25 - 35, The
textarea auto-resize logic in ElementRow currently only runs inside the onChange
handler, so it won’t update when description changes from outside the input,
such as after handleAICreate or on initial mount. Move the height adjustment
into ElementRow using a ref on the textarea plus a useEffect that runs whenever
description changes, and reuse the same scrollHeight-based sizing there so the
inline height is recalculated after remounts and external value updates.

Comment on lines +8 to +15
interface PatentImportModalProps {
initialPatentNumber: string;
onClose: () => void;
onSubmit: (data: { patentNumber: string }) => void;
}

export function PatentImportModal({ onClose, onSubmit }: PatentImportModalProps) {
const [patentNumber, setPatentNumber] = useState("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

initialPatentNumber prop이 선언되었지만 사용되지 않습니다.

인터페이스에 initialPatentNumber가 정의되어 있으나, 컴포넌트 destructuring에서 제외되고 useState("")로 초기화됩니다. prop을 사용하거나 인터페이스에서 제거하세요.

🤖 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 `@src/components/search/PatentImportModal.tsx` around lines 8 - 15, The
PatentImportModal props include initialPatentNumber, but the component ignores
it and always starts patentNumber with an empty string. Update PatentImportModal
to either use initialPatentNumber when initializing the useState in
PatentImportModal or remove initialPatentNumber from PatentImportModalProps if
it is not needed; make sure the destructuring matches the intended behavior.

Comment on lines +17 to +47
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-scrim-2"
onClick={onClose}
>
<div
className="w-138.5 p-6 flex flex-col gap-8 rounded-lg bg-bg-surface shadow-[0px_1px_6px_0px_rgba(144,155,165,0.36)]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between">
<h2 className="text-title-emphasis-17 text-title-primary">특허번호로 기술 불러오기</h2>
<button
type="button"
onClick={onClose}
aria-label="닫기"
className="flex items-center justify-center cursor-pointer"
>
<CancelIcon className="h-6 w-6" aria-hidden />
</button>
</div>

<TextField
label="특허번호"
value={patentNumber}
onChange={(e) => setPatentNumber(e.target.value)}
placeholder="특허번호를 입력해주세요"
/>

<Button onClick={() => onSubmit({ patentNumber })}>기술 불러오기</Button>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

모달 접근성 속성 및 키보드 처리가 누락되었습니다.

role="dialog", aria-modal="true", aria-labelledby가 없고, Escape 키 처리와 포커스 트랩이 없습니다. 스크린 리더 및 키보드 사용자 접근성이 보장되지 않습니다.

♿ 제안: 접근성 속성 및 Escape 키 추가
+ useEffect(() => {
+   const handleKeyDown = (e: KeyboardEvent) => {
+     if (e.key === "Escape") onClose();
+   };
+   document.addEventListener("keydown", handleKeyDown);
+   return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [onClose]);
+
  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-scrim-2"
+     role="dialog"
+     aria-modal="true"
+     aria-labelledby="patent-modal-title"
      onClick={onClose}
    >
🤖 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 `@src/components/search/PatentImportModal.tsx` around lines 17 - 47, The modal
in PatentImportModal is missing dialog accessibility and keyboard support.
Update the top-level overlay/dialog container to behave as a proper dialog by
adding role="dialog", aria-modal="true", and aria-labelledby tied to the heading
in the modal header, and ensure the close behavior handles Escape as well as
click/close button actions. Also add focus management so focus is trapped within
the modal while open and restored on close, using the modal container and the
existing onClose/onSubmit handlers to locate the changes.

placeholder="특허번호를 입력해주세요"
/>

<Button onClick={() => onSubmit({ patentNumber })}>기술 불러오기</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

빈 특허번호 제출이 검증되지 않습니다.

patentNumber가 빈 문자열이어도 onSubmit({ patentNumber })가 호출됩니다. 최소한 빈 값 검증 또는 disabled 처리를 추가하세요.

🛡️ 제안: 빈 값 검증 추가
- <Button onClick={() => onSubmit({ patentNumber })}>기술 불러오기</Button>
+ <Button
+   disabled={!patentNumber.trim()}
+   onClick={() => onSubmit({ patentNumber })}
+ >
+   기술 불러오기
+ </Button>
📝 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
<Button onClick={() => onSubmit({ patentNumber })}>기술 불러오기</Button>
<Button
disabled={!patentNumber.trim()}
onClick={() => onSubmit({ patentNumber })}
>
기술 불러오기
</Button>
🤖 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 `@src/components/search/PatentImportModal.tsx` at line 45, The
PatentImportModal submit action currently calls onSubmit from the Button click
even when patentNumber is empty, so add a guard in the PatentImportModal
component before invoking onSubmit and/or disable the Button until a non-empty
patentNumber is entered. Use the existing patentNumber state and the Button
onClick handler to prevent submitting blank values, and keep the fix localized
to PatentImportModal so the submit path is only reachable with a valid patent
number.

Comment on lines +14 to +17
<div className="flex flex-row gap-2 items-center">
<p className="text-title-primary text-title-22">{title}</p>
<p className="text-caption-label text-label-15">{label}</p>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

label이 undefined일 때 빈 <p> 요소가 렌더링됩니다.

label prop은 optional이지만 항상 <p>를 렌더링하므로, label이 없을 때 불필요한 빈 요소가 DOM에 남고 gap-2로 인해 여분의 간격이 생깁니다. 조건부 렌더링을 추가하세요.

♻️ 제안: label 조건부 렌더링
  <div className="flex flex-row gap-2 items-center">
    <p className="text-title-primary text-title-22">{title}</p>
-   <p className="text-caption-label text-label-15">{label}</p>
+   {label && <p className="text-caption-label text-label-15">{label}</p>}
  </div>
📝 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
<div className="flex flex-row gap-2 items-center">
<p className="text-title-primary text-title-22">{title}</p>
<p className="text-caption-label text-label-15">{label}</p>
</div>
<div className="flex flex-row gap-2 items-center">
<p className="text-title-primary text-title-22">{title}</p>
{label && <p className="text-caption-label text-label-15">{label}</p>}
</div>
🤖 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 `@src/components/search/SearchTitle.tsx` around lines 14 - 17, The SearchTitle
component is always rendering the label <p> even when the label prop is
undefined, which leaves an empty DOM node and extra spacing from the flex gap.
Update SearchTitle so the label element is conditionally rendered only when
label has a value, keeping the title row layout intact and removing the
unnecessary empty <p>.

@@ -0,0 +1,24 @@
import { forwardRef } from "react";
import { cn } from "@/lib/cn";
import AICreationIcon from "@/components/icons/icon-aiCreation.svg";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

빌드 실패: SVG 모듈을 찾을 수 없습니다.

@/components/icons/icon-aiCreation.svg 경로가 Turbopack 빌드에서 해결되지 않아 CI가 실패하고 있습니다. 파일이 존재하는지, 혹은 경로나 파일명(대소문자)이 정확한지 확인해 주세요.

#!/bin/bash
# icon-aiCreation.svg 파일 존재 여부 확인
fd -i "icon-aiCreation" src/components/icons/
# 또는 디렉토리 내 모든 svg 파일 확인
ls -la src/components/icons/
🧰 Tools
🪛 GitHub Actions: Preview / 0_vercel-preview.txt

[error] 3-3: Next.js/Turbopack build failed: Module not found. Can't resolve '@/components/icons/icon-aiCreation.svg' imported in AI_Creation_Button.tsx.

🪛 GitHub Actions: Preview / vercel-preview

[error] 3-3: Build error (Turbopack): Module not found. Can't resolve '@/components/icons/icon-aiCreation.svg' (imported by AI_Creation_Button.tsx:3:1).

🤖 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 `@src/components/ui/AI_Creation_Button.tsx` at line 3, The import in
AI_Creation_Button is failing because the SVG module path cannot be resolved by
Turbopack. Check that the icon file actually exists under the icons directory
and that the filename and casing exactly match the AICreationIcon import path;
if the asset was renamed or moved, update the import to the correct existing SVG
path or restore the missing file so the build can resolve it.

Source: Pipeline failures

Comment on lines +5 to +14
const labelVariants = cva("flex items-center text-title-secondary", {
variants: {
labelSize: {
13: "h-5 text-label-13",
15: "h-5 text-label-15",
17: "h-6 text-label-17",
},
},
defaultVariants: { labelSize: 13 },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

labelVariantsTextField와 중복 정의되어 있습니다.

동일한 labelVariants cva 설정이 TextAreaTextField에 각각 복사되어 있습니다. 공통 파일로 추출하여 DRY 원칙을 준수하고 향후 스타일 변경 시 일관성을 유지하세요.

♻️ 제안하는 리팩토링
+// src/components/ui/labelVariants.ts
+import { cva, type VariantProps } from "class-variance-authority";
+
+export const labelVariants = cva("flex items-center text-title-secondary", {
+  variants: {
+    labelSize: {
+      13: "h-5 text-label-13",
+      15: "h-5 text-label-15",
+      17: "h-6 text-label-17",
+    },
+  },
+  defaultVariants: { labelSize: 13 },
+});
+
+export type LabelSizeProps = VariantProps<typeof labelVariants>;

그 다음 두 컴포넌트에서 import하여 사용:

-import { cva, type VariantProps } from "class-variance-authority";
+import { labelVariants, type LabelSizeProps } from "`@/components/ui/labelVariants`";
🤖 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 `@src/components/ui/TextArea.tsx` around lines 5 - 14, `labelVariants` is
duplicated between `TextArea` and `TextField`; extract the shared `cva`
definition into a common reusable module and import it from both components.
Move the existing `labelVariants` configuration (including the `labelSize`
variants and default) into a single shared symbol, then update `TextArea` and
`TextField` to reference that shared export so styling stays consistent and
future changes only happen in one place.

@minsxo minsxo merged commit 042dd75 into main Jul 8, 2026
1 of 2 checks passed
GirimNam added a commit to GirimNam/IPX-FE that referenced this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants