Skip to content

Refactor(client): PromptInput 컴포넌트 리디자인 적용 - #272

Open
twossu wants to merge 22 commits into
developfrom
refactor/prompt-input-component-redesign/#266
Open

Refactor(client): PromptInput 컴포넌트 리디자인 적용#272
twossu wants to merge 22 commits into
developfrom
refactor/prompt-input-component-redesign/#266

Conversation

@twossu

@twossu twossu commented Jul 30, 2026

Copy link
Copy Markdown
Member

📌 Summary

AI 패널의 프롬프트 입력 영역(PromptInput/PromptOption/SelectedMemo)을 ai-panel 폴더로 재배치하고 선택된 메모 노출/삭제 textarea 자동 높이 조정 등 UX 디테일을 다듬은 리팩터링입니다. 스타일 측면에서는 border 렌더링 방식을 outline → inset box-shadow로 바꿔 레이아웃 흔들림 없이 focus 상태를 표현하도록 정리했습니다.

📚 Tasks

  • 구조 정리: PromptInput, PromptOptionai-prompt/componentsai-panel/prompt-input으로 이동
  • 선택된 메모 UI: selectedMemosCount(숫자) → selectedMemos(배열) + onRemoveMemo로 교체, 메모 칩 노출·개별 삭제·가로 스크롤 지원
  • 입력 UX 개선: textarea 자동 높이 조정, 전송 가능 조건(canSend) 단순화
  • 스타일 정리: border → inset box-shadow 전환, PromptOption 재클릭 시 선택 해제 로직 제거, sm 버튼 비활성 배경색 수정, Tooltip 줄바꿈 방지
  • 네이밍 컨벤션: 콜백 prop handleXonX로 통일

🔍 Describe

구조 이동

지연님의 selected-memo PR 구조를 참고해서 PromptInput, PromptOptionai-panel 폴더로 옮겨 AI 관련 패널 컴포넌트들을 한 곳에서 관리하도록 정리했습니다.

선택된 메모 UI

기존에는 selectedMemosCount 숫자만 받아서 개수만 표시했는데 실제 메모 제목 노출·개별 삭제가 필요해져서 selectedMemos: {id, title}[] + onRemoveMemo로 교체했습니다. 각 메모는 SelectedMemo 칩으로 렌더링되고 개수가 많아지면 줄바꿈 대신 가로 스크롤됩니다.

textarea 자동 높이

입력 내용이 길어지면 min-height(6.6rem) ~ max-height(19.2rem) 범위 내에서 textarea 높이가 자동으로 늘어나도록 했습니다.

전송 가능 조건 정리

메모를 선택하지 않아도 전송 가능하도록 바꾼 이유: 이번 스프린트부터 ai-panel이 메모를 하나도 선택하지 않은 상태에서도 열릴 수 있게 되어서 텍스트 입력만 있으면 전송 가능하도록 canSend = hasValue && !disabled로 단순화했습니다. handleSend 내부에서도 한 번 더 가드해서 disabled 상태에서 우발적으로 제출되는 걸 막았습니다.

PromptOption 재클릭 시 선택 해제(toggle-off) 제거

(prompt-option.tsxhandleSelect)

토글-오프 로직을 제거한 이유: 원래는 선택된 옵션을 다시 클릭하면 선택이 풀리는 로직이 있었는데 이번 스프린트부터 라디오처럼 셋 중 하나는 항상 선택되어 있어야 하는 사양으로 바뀌어서 같은 옵션 재클릭은 무시하고 다른 옵션을 클릭할 때만 전환되도록 단순화했습니다.

border → inset box-shadow 전환 이유

어떻게 발견하게 됐는지
prompt-input 사이즈를 피그마랑 비교하는데 자꾸 +2px, +4px 이런 식으로 오차가 나더라고요. "이거 1px짜리 선들이 겹쳐서 계산되는 거 아닐까?" 싶어서 피그마를 다시 들여다봤는데 전역에 box-sizing: border-box가 이미 세팅되어 있더라고요?! (전 이게 세팅되어 있는 줄도 모르고 그냥 "선 안쪽으로 잘 그려지네~" 하면서 편하게 살아왔던 것 같습니다 ㅎㅎ)
image

근데 왜 전역 설정이 있는데도 선이 밖으로 그려졌을까
찾아보니 border-box가 적용돼 있어도 border가 진짜 안쪽으로 그려지려면 그 요소의 크기(width/height)가 고정값으로 지정돼 있어야 한다고 하더라고요. 이거 진짜 처음 알았어요! 그동안은 그냥 border 주고 크기 안 맞으면 padding으로만 조절했었는데 왜 자꾸 커지나 했더니 이런 이유가 있었네요.

그래서 컴포넌트별로 이렇게 적용했습니다

  • 크기가 고정된 selected-memo, prompt-option은 width/height를 지정해두고 그대로 border를 썼어요
메모 제 div selected-memo_contai
  • 근데 prompt-option-item은 크기가 고정인데도 왜 box-shadow를 썼냐면 아이콘 자체 크기가 32px인데 selected 상태 아이템 크기도 똑같이 32px이어서 그 안에 진짜 border를 그리면 아이콘이 안 들어맞더라고요. 그래서 여기도 안쪽 선을 쓸 수밖에 없었습니다.
  • prompt-input은 입력 글자 수·선택된 메모 유무에 따라 세로 크기가 계속 바뀌어서 아예 크기를 고정할 수가 없었어요. 다른 방법 없나 찾아봤는데 대부분 "크기 고정하고 border 쓰기" 아니면 "box-shadow 쓰기" 둘 중 하나더라고요. 그래서 결국 boxShadow: inset 0 0 0 1px color로 갔습니다.

그래도 아직 고민되는 부분
box-shadow로 하니까 뭔가 살짝 어색한 느낌이 있어서 다른 방법 없나 더 찾아봤는데 못 찾았어요... 혹시 좋은 방법 아시면 알려주세요!

콜백 prop 네이밍 컨벤션 정리

handleSubmit, handleOptionSelect처럼 prop 이름에 handle 접두사가 붙어 있던 걸 onSubmit, onOptionSelect로 바꿨습니다. "실제 로직을 가진 함수는 사용하는 쪽에서 handleX로 두고 컴포넌트가 외부에서 받는 이벤트 prop은 onX로 둔다"는 팀 컨벤션(#249 리뷰 코멘트)에 맞춘 변경입니다.

👀 To Reviewer

input 분량이 넘어가면 스크롤이 생기는데,브라우저 기본 스크롤이 서비스 디자인과 어울리지 않아서 디자이너분들께 커스텀 스크롤을 받았습니다. 앞으로 다른 곳에서도 스크롤이 많이 필요할 것 같은데 전역 스타일로 만드는 작업은 별도 PR에서 진행하는 것은 어떡신가요!

  • 테스트 가능하도록 ai-panel에 임시?로 교체했습니다!

📸 Screenshot

2026-07-31.12.35.39.mov
2026-08-02.6.37.49.mov

@twossu
twossu requested a review from a team as a code owner July 30, 2026 15:44
@twossu
twossu removed the request for review from a team July 30, 2026 15:44
@twossu twossu linked an issue Jul 30, 2026 that may be closed by this pull request
@twossu
twossu requested review from jm8468, jogpfls and jyeon03 July 30, 2026 15:44
@github-actions github-actions Bot added 🔨 Refactor 코드 구조 개선 🦦 최윤하 웹 37기 최윤하 labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🎨 Storybook 배포 완료

PR 작성자: @twossu

🔗 배포된 Storybook 보기

@twossu twossu changed the title refactor: PromptInput 컴포넌트 리디자인 및 ai-panel 구조 정리 refactor(client): PromptInput 컴포넌트 리디자인 Jul 30, 2026
@twossu twossu changed the title refactor(client): PromptInput 컴포넌트 리디자인 Refactor(client): PromptInput 컴포넌트 리디자인 Jul 30, 2026
@twossu twossu changed the title Refactor(client): PromptInput 컴포넌트 리디자인 Refactor(client): PromptInput 컴포넌트 리디자인 적용 Jul 30, 2026

@jm8468 jm8468 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

전역 스크롤은 별도 PR에서 진행하는거 좋네용
수고하셨어요!

Comment thread apps/client/src/shared/components/ai-panel/prompt-input/prompt-input.tsx Outdated
Comment thread apps/client/src/shared/components/ai-panel/prompt-input/prompt-input.tsx Outdated
Comment thread apps/client/src/shared/components/ai-panel/prompt-input/prompt-input.tsx Outdated
Comment on lines +34 to +42
value,
onChange,
selectedOptionId,
onOptionSelect,
onSubmit,
disabled = false,
selectedMemos = [],
onRemoveMemo,
isDragOver = false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

props에 대해 다시 고민해보면 어떨까용??
컴포넌트의 props를 설계할 때 고려되는 기준 중 하나는 API를 어느 컴포넌트에서 호출할 것인가 에요.
동시에 API의 호출 위치를 결정하는 기준 중 하나는 관련된 것은 가장 가까이 있어야 한다 에요.

PromptInput 내에는 선택된 메모를 관리하고 / textarea에 내용을 입력하고 / 제출 버튼을 통해 내용을 전송하고 있어요.
현재 스웨거에서 어떤 값을 담아 요청을 보내야 하는지 확인하면 props를 줄일 수 있다고 생각해요!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요 코멘트에 대한 내용만은 아니고 코드리뷰 전체에서 적용할 수 있는 내용인데, 저도 도움이 됐어서 공유드립니당


리뷰를 보고 특정 방식으로 진행하겠다는 생각이 서더라도, 불확실하거나 리뷰어가 함께 고민해주길 바라는 부분은 코드로 작성하기 전에 미리 공유해주셔도 굿굿입니다!

코드로 작성하기 전에 티키타카로 합의하고 커밋하면 리뷰어가 납득하기 쉽고 편해져요.
반대로 리뷰에 대해 혼자 판단해서 반영하고 커밋을 올리면 리뷰어가 그 반영 커밋의 의도를 확인하는 데에도 시간이 들어요
-> 결과적으로 시간을 아끼려던 선택이 더 느려지게 돼요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

refactor: PromptInput 컴포넌트를 AiPrompt에 통합하고 props 정리

좋은 기준 감사합니다!!! 말씀하신 기준으로 다시 보니 value, onChange, selectedOptionId, onOptionSelect는 결국 PromptInput 내부에서 텍스트 입력과 옵션 선택을 관리하는 데만 쓰이고 있어서 굳이 상위 컴포넌트에서 내려받을 필요가 없네요. 그래서 useState로 컴포넌트 내부에서 관리하도록 옮기고 제출 시점에 필요한 값만 onSubmit으로 넘기도록 정리했습니다!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

굿굿!!!

onSubmit도 props로 사용하지 않아도 될 것 같은데 한 번 확인해주시겠어여??

스웨거를 보면
요청 시 포함할 데이터는 아래와 같아요

  • chatRoomId : AI 대화 세션 ID
  • userPrompt : 사용자의 질문
  • option : AI 처리 전략 (MERGE, STRUCTURE, SUMMARY)
  • memoIds : 참조할 메모 ID 목록

MemoInput 컴포넌트 내부에서 userPrompt, option, memoIds를 관리하고 있어서 props로 받지 않아도 내부에서 해결할 수 있어 보여요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

상위 컴포넌트에서 조합해서 쓰려면 제출 트리거가 필요할 것 같아서 onSubmit으로 필요할 것 같아요!

이 컴포넌트에서 API를 직접 호출하는 구조로 바꾸면 성공/실패에 따른 messages 리스트 관리 여러 mutation을 합친 isLoading 같은 걸 PromptInput이 추가로 알아야 해서 오히려 결합도가 올라갈 것 같아요.

그래서 onSubmit으로 요청에 필요한 값(userPrompt, option)만 올려주도록 했는데 제가 질문을 잘못 이해한 것이라면 알려주세요!

@jm8468 jm8468 Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

레포를 좀 찾아봤는데, use-ai-prompt 훅에서 mutation 관련 로직이 합쳐져 있었군요!
저희가 각자 알고 있는 코드 맥락이 달라서 한번 더 질문드리게 됐던 것 같아요

그렇다면 윤하님 의견대로 가면 좋을 것 같습니다 :D

Comment thread apps/client/src/shared/components/ai-panel/prompt-input/prompt-input.tsx Outdated
Comment thread apps/client/src/shared/components/ai-panel/prompt-input/prompt-input.tsx Outdated

@jogpfls jogpfls left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

구현하시느라 수고하셨어요 !! 어려우셨을텐데,, 역시 윤하리드님이네요,, 👍🏻
궁금한 점이랑 확인하면 좋을 부분 코멘트로 남겼으니까 확인 부탁드려요 !

}, [value]);

const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== 'Enter' || e.shiftKey) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (e.key !== 'Enter' || e.shiftKey) return;
if (e.key !== 'Enter' || e.shiftKey || event.nativeEvent.isComposing) return;

지연님 PR에서도 언급하긴 했는데 한글은 IEM 조합 문자라서, 한글이 조합중이라고 판단하면 조합 완료, 키 누름 완료, 이렇게 2번 인식해서 엔터키를 눌렀을 때 2번 전송이 될 수 있어요 ! 그래서 event.nativeEvent.isComposing 이걸 처리해줘야 한 번만 전송될거에요 !

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

덜덜 지연님 PR에서 코멘트 보고 "오 신기하네" 하고 그냥 지나갔었는데 생각해보니 저도 같은 상황이었네요 ㅎㅎㅎㅎ

input에 이런 방어 로직을 넣어야 한다는 걸 이제야 알았어요. 이 로직 꼭 기억해두고 잊지 않을게요 🙏

Comment on lines +42 to +48
useLayoutEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;

textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}, [value]);

@jogpfls jogpfls Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

나중에,,,,,,,, height을 측정해서 height을 return 하는 공용 유틸로 빼도 좋을 것 같아용 !!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

근데 useLayoutEffect 말고 textarea에서 row 속성을 사용하거나 최대 8줄을 사용해서 css로 처리하는 방법은 없을까요 ?? JW

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

나중에 높이를 측정하는 곳이 있다면 공용 으로 빼면 좋을 것 같아욥!

왜 useLayoutreffect로 처리했냐며언! row랑 CSS는 정적인 값이라 지금 몇 줄인지 계산할 수 없어서 JS로 측정하도록 처리했습니다!

저도 다른 방법이 있을지 자동 조절 되는textarea 구현하기 여러 사람들 블로그를 보니깐 다들 이렇게 구현했더라고요!

<div className={styles.footer}>
<PromptOption
selectedOptionId={selectedOptionId}
onOptionSelect={setSelectedOptionId}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

setter 함수를 바로 넘기고 있는데 여기서는 큰 문제가 되진 않지만 한 번쯤 알면 좋을 것 같아서 이야기해봅니다 !

재사용 컴포넌트에서는 setter 함수를 넘겨주는건 의논이 많이 갈리는 방법이에요 ! ! 물론 여기는 재사용 컴포넌트는 아니라서 큰 문제는 안되지만요 !

자식 컴포넌트가 부모 컴포넌트의 정보를 많이 알게 되는 문제도 있고, 나중에 요구되는 기능이 있어서 넘겨야하는 함수 하나가 추가된다면 새로운 함수를 만들어야 한다는 단점이 있어요. 물론 setter함수를 그대로 넘기지 않는다면 의미없는 함수 래핑이 된다며 setter 함수를 props로 바로 넘겨주는 것에 있어서 문제가 안된다는 의견도 있고요

레퍼런스: https://matanbobi.dev/posts/stop-passing-setter-functions-to-components

근데 제가 안에 PromptOption 함수를 보니까 안에서 한 번 더 래핑해주고 있더라고요

//prompt-option.tsx

const handleSelect = (id: string) => {
  if (selectedOptionId === id) return;
  onOptionSelect(id);
};

차라리 여기 파일에서 위에 로직을 처리해주시면 좋은데 어떻게 생각하실까용가리?

@twossu twossu Aug 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

헉 매번 상위에서 로직을 관리할 수 있도록 신경 쓰려고 했는데 prompt-item의 상위가 PromptOption이니까 여기서 처리하면 되겠다고 단순하게 생각해버렸네요!

지금은 재사용 컴포넌트가 아니지, 코드 통일성과 제 습관 개선을 위해 PromptInput에서 처리하도록 수정했습니다!

또 지금은 재사용 컴포넌트가 아니더라도 나중에 재사용하게 되는 경우가 종종 있으니 미리 수정해두는 게 좋을 것 같아요.

refactor: PromptOption의 옵션 선택 가드 로직을 상위 컴포넌트로 이동


interface PromptInputValueType {
text: string;
selectedOptionId: string | null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

위에서

type OptionType = 'MERGE' | 'SUMMARY' | 'STRUCTURE';

처리 해주고

Suggested change
selectedOptionId: string | null;
selectedOptionId: OptionType;

이 타입 사용하면 어떨까용 ??

이번 스프린트부터 라디오처럼 셋 중 하나는 항상 선택되어 있어야 하는 사양으로 바뀌어서

위와 같은 상황이라면 null이 필요없고 셋 중에 하나 강제하면 될 것 같은데 null 타입이 필요할까요 ?!
안에 prompt-option에서도 위와 관련해서 타입 반영해주면 좋을 것 같아요 !!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

prompt-input에서 타입을 스키마의 값을 바로 받아서 사용하도록 수정하면서 같이 수정완료했습니다1!

refactor: PromptInput 컴포넌트의 타입을 스키마 기반으로 정리


interface PromptInputValueType {
text: string;
selectedOptionId: string | null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

뭔가 뒤에 id를 붙히니까 숫자?같은게 올 것 같은데,, 저만 그런가요 ?? 저만 그런거일수도 있어서,,, 이거는 윤하님 기호에 맞게 생각해주십쇼,,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

prompt-input에서 타입을 스키마의 값을 바로 받아서 사용하도록 수정하면서 같이 수정완료했습니다1!

refactor: PromptInput 컴포넌트의 타입을 스키마 기반으로 정리

}: PromptInputProps) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [value, setValue] = useState('');
const [selectedOptionId, setSelectedOptionId] = useState<string | null>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
const [selectedOptionId, setSelectedOptionId] = useState<string | null>(
const [selectedOptionId, setSelectedOptionId] = useState<OptionType>(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

prompt-input에서 타입을 스키마의 값을 바로 받아서 사용하도록 수정하면서 같이 수정완료했습니다1!

refactor: PromptInput 컴포넌트의 타입을 스키마 기반으로 정리
refactor: PromptInput/PromptOption의 option 타입을 PromptInputValueType 기…

@jm8468
jm8468 self-requested a review August 7, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 코드 구조 개선 🦦 최윤하 웹 37기 최윤하

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] prompt-input 컴포넌트 리디자인 반영

3 participants