Repository: LL-EZGG/ezgg_2
Pull request #49: Feat/#40 자연어 입력창 구현
Author: login: chanchangood
Created: 2025-05-15T10:02:42Z | Merged: 2025-05-16T07:10:46Z

## 🔎 작업 내용

- SectionSelector.jsx 파일에 주석 추가
- DuoFinderForm.jsx 파일 line break, 띄어쓰기 리포맷
- 사용자가 텍스트를 입력할 수 있는 폼 추가
- 30글자까지 입력할 수 있도록 설정
- 백엔드 dto 스키마에 텍스트를 담을 필드 추가
- useWebSocket.js 파일에 주석 추가
- 백엔드에 form 입력 정보 전송 요청 전에 dto로 변환하도록 수정

- 불필요한 주석 삭제
- 불필요한 export문 삭제
- @JsonProperty 어노테이션을 안정성을 높이고 통일감을 주기 위해 추가
- 최신 dev 변경사항 conflict 해결
- 들여쓰기 조정

## 🔧 앞으로의 과제

임베딩 모델 이식




Diff:
diff --git a/backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java b/backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java
index 06b2eeed..d01d0ede 100644
--- a/backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java
+++ b/backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java
@@ -17,10 +17,11 @@
 @ToString
 public class PreferredPartnerParsingDto {
 
+	@JsonProperty("wantLine")
 	private WantLine wantLine;
-
 	@JsonProperty("selectedChampions")
 	private ChampionInfo championInfo;
+	private String userPreferenceText;
 
 	@Getter
 	@NoArgsConstructor
@@ -38,6 +39,7 @@ public static class WantLine {
 	@Builder
 	@ToString
 	public static class ChampionInfo {
+		@JsonProperty("preferredChampions")
 		private List<String> preferredChampions;
 		@JsonProperty("bannedChampions")
 		private List<String> unpreferredChampions;
diff --git a/frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx b/frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx
index 4ade17bd..d68e8a7a 100644
--- a/frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx
+++ b/frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx
@@ -2,49 +2,54 @@ import styled from '@emotion/styled';
 import SectionSelector from "./SectionSelector.jsx";
 
 const DuoFinderForm = (
-  {
-    matchingCriteria,
-    setMatchingCriteria,
-  }) => {
+    {
+        matchingCriteria,
+        setMatchingCriteria,
+    }) => {
 
-  return (
-    <Form>
-      <SectionSelector
-        matchingCriteria={matchingCriteria}
-        setMatchingCriteria={setMatchingCriteria}
-        type={'line'}
-        kind={'my'}
-        selectedValue={matchingCriteria.wantLine.myLine}
-        disabledValue={matchingCriteria.wantLine.partnerLine}
-      />
-      <SectionSelector
-        matchingCriteria={matchingCriteria}
-        setMatchingCriteria={setMatchingCriteria}
-        type={'line'}
-        kind={'partner'}
-        selectedValue={matchingCriteria.wantLine.partnerLine}
-        disabledValue={matchingCriteria.wantLine.myLine}
-      />
-      <SectionSelector
-        matchingCriteria={matchingCriteria}
-        setMatchingCriteria={setMatchingCriteria}
-        type={'champion'}
-        kind={'preferred'}
-      />
-      <SectionSelector
-        matchingCriteria={matchingCriteria}
-        setMatchingCriteria={setMatchingCriteria}
-        type={'champion'}
-        kind={'banned'}
-      />
-    </Form>
-  );
+    return (
+        <Form>
+            <SectionSelector
+                matchingCriteria={matchingCriteria}
+                setMatchingCriteria={setMatchingCriteria}
+                type={'line'}
+                kind={'my'}
+                selectedValue={matchingCriteria.wantLine.myLine}
+                disabledValue={matchingCriteria.wantLine.partnerLine}
+            />
+            <SectionSelector
+                matchingCriteria={matchingCriteria}
+                setMatchingCriteria={setMatchingCriteria}
+                type={'line'}
+                kind={'partner'}
+                selectedValue={matchingCriteria.wantLine.partnerLine}
+                disabledValue={matchingCriteria.wantLine.myLine}
+            />
+            <SectionSelector
+                matchingCriteria={matchingCriteria}
+                setMatchingCriteria={setMatchingCriteria}
+                type={'champion'}
+                kind={'preferred'}
+            />
+            <SectionSelector
+                matchingCriteria={matchingCriteria}
+                setMatchingCriteria={setMatchingCriteria}
+                type={'champion'}
+                kind={'banned'}
+            />
+            <SectionSelector
+                matchingCriteria={matchingCriteria}
+                setMatchingCriteria={setMatchingCriteria}
+                type={'userPreferenceText'}
+            />
+        </Form>
+    );
 };
 
 export default DuoFinderForm;
 
 const Form = styled.form`
-  display: flex;
-  flex-direction: column;
-  gap: 1.5rem;
+    display: flex;
+    flex-direction: column;
+    gap: 1.5rem;
 `;
diff --git a/frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx b/frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx
index d31378d7..07acb899 100644
--- a/frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx
+++ b/frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx
@@ -1,17 +1,42 @@
+/** -----------------------------------------------------------------------------
+ * SectionSelector.jsx
+ * -----------------------------------------------------------------------------
+ * 매칭 조건 설정 폼에서 사용되는 다목적 셀렉터 컴포넌트.
+ * - 라인(TOP/JUG/MID/AD/SUP) 선택, 챔피언 선호/비선호 선택, 사용자 메모(30자) 입력 세 가지 모드를 가짐.
+ * - props 로 전달받은 matchingCriteria 를 직접 수정하지 않고, 반드시 setMatchingCriteria
+ *   콜백을 통해 상위 상태를 업데이트한다.
+ * ---------------------------------------------------------------------------*/
 import styled from '@emotion/styled';
 import React, {useEffect, useRef, useState} from "react";
 import {champions} from "../../data/champions.js";
 
-const lines = ['TOP', 'JUG', 'MID', 'AD', 'SUP'];
+/* ------------- 상수 ---------------------------------------------------------------- */
 
+// 라인 버튼에 표시될 라벨 목록
+const lines = ['TOP', 'JUG', 'MID', 'AD', 'SUP'];
+// 사용자 메모 최대 글자 수
+const TEXT_LIMIT = 30;
+
+/* ------------- 보조 함수: 라인 선택 -------------------------------------------------------- */
+
+/**
+ * 라인 버튼 클릭 시 상태를 토글하는 함수
+ * 같은 라인을 my/partner 양쪽에서 중복 선택하지 않도록 처리한다.
+ * @param {('myLine'|'partnerLine')} type            - state 키(my 라인인지 partner 라인인지)
+ * @param {string} line                              - 클릭된 라인 값
+ * @param {object} matchingCriteria                  - 현재 매칭 기준 상태 객체
+ * @param {Function} setMatchingCriteria             - 상태 업데이트 콜백
+ */
 const handleLineSelect = (type, line, matchingCriteria, setMatchingCriteria) => {
   const currentWantLine = matchingCriteria?.wantLine || {};
   const currentMyLine = currentWantLine.myLine || '';
   const currentPartnerLine = currentWantLine.partnerLine || '';
 
+  // 중복 선택 상황 방지
   if (type === 'partnerLine' && line === currentMyLine) return;
   if (type === 'myLine' && line === currentPartnerLine) return;
 
+  // 토글 로직
   const newWantLine = {
     ...currentWantLine,
     [type]: currentWantLine[type] === line ? '' : line
@@ -23,28 +48,51 @@ const handleLineSelect = (type, line, matchingCriteria, setMatchingCriteria) =>
   });
 };
 
+/* ------------- 메인 컴포넌트 ---------------------------------------------------------- */
+
+/**
+ * SectionSelector 컴포넌트
+ * ---------------------------------------------------------------------------
+ * 라인 선택 UI 혹은 챔피언 선택 UI를 렌더링하는 함수형 컴포넌트.
+ *
+ * @prop {object}   matchingCriteria          - 상위 컴포넌트의 상태 객체
+ * @prop {Function} setMatchingCriteria       - 상태 변경 함수(상태를 불변성 지켜 갱신)
+
+ * @prop {'line'|'champion'|'userPreferenceText'} type - 라인 선택 모드 or 챔피언 선택 모드 or 텍스트 입력 모드
+ * @prop {'my'|'partner'|'preferred'|'banned'} kind - 세부 유형(라인: my/partner, 챔피언: preferred/banned)
+
+ * @prop {string}   selectedValue             - (라인 모드) 현재 선택된 값
+ * @prop {string}   disabledValue             - (라인 모드) 클릭 불가능한 값(상대가 이미 선택한 라인)
+ */
 const SectionSelector = ({
                            matchingCriteria,
                            setMatchingCriteria,
-                           type, // 'line' 또는 'champion'
-                           kind, // 'my'/'partner' 또는 'preferred'/'banned'
+                           type,
+                           kind,
                            selectedValue,
                            disabledValue,
-                           children
+                           children // 향후 확장용 (현재 미사용)
                          }) => {
 
-  const [searchTerm, setSearchTerm] = useState('');
-  const [bannedSearchTerm, setBannedSearchTerm] = useState('');
-  const [showSuggestions, setShowSuggestions] = useState(false);
-  const [showBannedSuggestions, setShowBannedSuggestions] = useState(false);
-  const [selectedIndex, setSelectedIndex] = useState(0);
-  const [bannedSelectedIndex, setBannedSelectedIndex] = useState(0);
-  const searchRef = useRef(null);
-  const bannedSearchRef = useRef(null);
-  const preferredSuggestionsRef = useRef(null);
-  const bannedSuggestionsRef = useRef(null);
+  /* ----- 상태: 챔피언 검색어 및 UI 토글 ----------------------------------- */
+  const [searchTerm, setSearchTerm] = useState(''); // 선호 챔피언 검색어
+  const [bannedSearchTerm, setBannedSearchTerm] = useState(''); // 비선호 챔피언 검색어
+
+  const [showSuggestions, setShowSuggestions] = useState(false); // 선호 챔피언 추천박스
+  const [showBannedSuggestions, setShowBannedSuggestions] = useState(false);  // 비선호 챔피언 추천박스
+
+  const [selectedIndex, setSelectedIndex] = useState(0); // 선호 챔피언 추천박스 활성 인덱스
+  const [bannedSelectedIndex, setBannedSelectedIndex] = useState(0); // 비선호 챔피언 추천박스 활성 인덱스
+
+  /* ----- ref: DOM 요소 참조 ---------------------------------------------- */
+  const searchRef = useRef(null); // 선호 입력창 wrapper
+  const bannedSearchRef = useRef(null); // 비선호 입력창 wrapper
+  const preferredSuggestionsRef = useRef(null); // 선호 챔피언 추천 목록
+  const bannedSuggestionsRef = useRef(null); // 비선호 챔피언 추천 목록
 
+  /* ------------------ effect: 외부 클릭 시 추천 박스 닫기 ------------------ */
   useEffect(() => {
+    /** 외부 클릭을 감지해 추천 박스를 닫는 함수. */
     const handleClickOutside = (event) => {
       if (searchRef.current && !searchRef.current.contains(event.target)) {
         setShowSuggestions(false);
@@ -58,6 +106,7 @@ const SectionSelector = ({
     return () => document.removeEventListener('mousedown', handleClickOutside);
   }, []);
 
+  /* ----- effect: 선택 인덱스 변경 시 스크롤 위치 맞추기 ------------------------ */
   useEffect(() => {
     scrollToSelected(selectedIndex, 'preferred');
   }, [selectedIndex]);
@@ -66,6 +115,9 @@ const SectionSelector = ({
     scrollToSelected(bannedSelectedIndex, 'banned');
   }, [bannedSelectedIndex]);
 
+  /* ---------------- 입력/포커스 핸들러 ----------------------------------- */
+
+  /** 검색어가 변경될 때 상태를 업데이트하는 함수. */
   const handleChange = (e, kind) => {
     if (kind === 'preferred') {
       setSearchTerm(e.target.value);
@@ -78,6 +130,7 @@ const SectionSelector = ({
     }
   };
 
+  /** 입력창 포커스 시 추천 박스를 여는 함수. */
   const handleShowSuggestions = (kind) => {
     if (kind === 'preferred') {
       setShowSuggestions(true);
@@ -86,6 +139,7 @@ const SectionSelector = ({
     }
   }
 
+  /** 선택된 추천 항목이 보이도록 스크롤을 이동하는 함수.*/
   const scrollToSelected = (index, type) => {
     const suggestionsElement = type === 'preferred' ?
       preferredSuggestionsRef.current :
@@ -111,6 +165,7 @@ const SectionSelector = ({
     }
   };
 
+  /** 키보드 입력(▲▼Enter)을 처리하는 함수. */
   const handleKeyDown = (e, type) => {
     const suggestions = filterChampions(type === 'preferred' ? searchTerm : bannedSearchTerm);
     const currentIndex = type === 'preferred' ? selectedIndex : bannedSelectedIndex;
@@ -144,6 +199,7 @@ const SectionSelector = ({
     }
   };
 
+  /** 검색어로 챔피언 배열을 필터링하는 함수. */
   const filterChampions = (term) => {
     if (!term) return champions;
     const lowerTerm = term.toLowerCase();
@@ -153,6 +209,7 @@ const SectionSelector = ({
     );
   };
 
+  /** 챔피언을 선택하여 상태에 반영하는 함수. */
   const handleChampionSelect = (champion, type) => {
     const key = type === 'preferred' ? 'preferredChampions' : 'bannedChampions';
     const otherKey = type === 'preferred' ? 'bannedChampions' : 'preferredChampions';
@@ -190,6 +247,7 @@ const SectionSelector = ({
     }
   };
 
+  /** 선택된 챔피언 태그를 제거하는 함수. */
   const handleRemoveChampion = (championId, type) => {
     const key = type === 'preferred' ? 'preferredChampions' : 'bannedChampions';
 
@@ -204,6 +262,9 @@ const SectionSelector = ({
     setMatchingCriteria(newCriteria);
   };
 
+
+  /* ---------------- 렌더링: 라인 선택 모드 --------------------------------- */
+
   if (type === 'line') {
     return (
       <Section>
@@ -231,10 +292,14 @@ const SectionSelector = ({
       </Section>
     );
   } else if (type === 'champion') {
+
+    /* ---------------- 렌더링: 챔피언 선택 모드 ------------------------------- */
+
     return (
       <Section>
         <Label>{kind === 'preferred' ? '선호 챔피언' : '비선호 챔피언'}</Label>
         <SearchContainer ref={kind === 'preferred' ? searchRef : bannedSearchRef}>
+          {/* 검색 입력 */}
           <SearchInput>
             <input
               type="text"
@@ -247,6 +312,8 @@ const SectionSelector = ({
               onKeyDown={(e) => handleKeyDown(e, kind)}
             />
           </SearchInput>
+
+          {/* 추천 목록 */}
           {(() => {
             const {
               show,
@@ -282,6 +349,8 @@ const SectionSelector = ({
               </Suggestions>
             );
           })()}
+
+          {/* 선택된 챔피언 태그 */}
           <ChampionTags>
             {(kind === 'preferred'
                 ? matchingCriteria.selectedChampions.preferredChampions
@@ -302,13 +371,36 @@ const SectionSelector = ({
         </SearchContainer>
       </Section>
     );
+  } else if (type === 'userPreferenceText'){
+
+    /* ---------------- 렌더링: 사용자 메모 입력 ------------------------------ */
+
+    return (
+        <Section>
+          <Label>원하는 상대의 플레이 스타일</Label>
+          <TextInput
+              type="text"
+              maxLength={TEXT_LIMIT}
+              placeholder="ex) 탱커를 잘함, 로밍을 잘감, ..."
+              value={matchingCriteria.userPreferenceText || ''}
+              onChange={(e) =>
+                  setMatchingCriteria({
+                    ...matchingCriteria,
+                    userPreferenceText: e.target.value.slice(0, TEXT_LIMIT),
+                  })
+              }
+          />
+        </Section>
+    );
   }
 
-  return null;
+  return null;  // 방어 코드
 };
 
 export default SectionSelector;
 
+/* ------------- 스타일 컴포넌트 --------------------------------------------- */
+
 const Section = styled.div`
   display: flex;
   flex-direction: column;
@@ -456,3 +548,15 @@ const ChampionTag = styled.div`
     }
   }
 `;
+
+const TextInput = styled.input`
+  width: 100%;
+  padding: 0.5rem 0.8rem;
+  background: rgba(255, 255, 255, 0.1);
+  border: none;
+  border-radius: 4px;
+  color: white;
+  font-size: 0.9rem;
+  &::placeholder { color: rgba(255, 255, 255, 0.5); }
+  &:focus { outline: none; }
+`;
diff --git a/frontend/ezgg/src/components/DuoFinder/matching/MatchingCriteriaInfo.jsx b/frontend/ezgg/src/components/DuoFinder/matching/MatchingCriteriaInfo.jsx
index b52a16b5..fdb54acd 100644
--- a/frontend/ezgg/src/components/DuoFinder/matching/MatchingCriteriaInfo.jsx
+++ b/frontend/ezgg/src/components/DuoFinder/matching/MatchingCriteriaInfo.jsx
@@ -5,24 +5,25 @@ const MatchingCriteriaInfo = ({ matchingCriteria }) => {
     <Container>
       <LoadingSpinner />
       <Message>듀오를 찾는 중입니다...</Message>
-      <Stats>
-        <p>내 선호 라인: {matchingCriteria.wantLine.myLine}</p>
-        <p>상대방 선호 라인: {matchingCriteria.wantLine.partnerLine}</p>
-        <p>
-          선호 챔피언: {
-          matchingCriteria.selectedChampions.preferredChampions.length > 0
-            ? matchingCriteria.selectedChampions.preferredChampions.map(c => c.name).join(', ')
-            : '없음'
-        }
-        </p>
-        <p>
-          비선호 챔피언: {
-          matchingCriteria.selectedChampions.bannedChampions.length > 0
-            ? matchingCriteria.selectedChampions.bannedChampions.map(c => c.name).join(', ')
-            : '없음'
-        }
-        </p>
-      </Stats>
+        <Stats>
+            <p>내 선호 라인: {matchingCriteria.wantLine.myLine}</p>
+            <p>상대방 선호 라인: {matchingCriteria.wantLine.partnerLine}</p>
+            <p>
+                선호 챔피언: {
+                matchingCriteria.selectedChampions.preferredChampions.length > 0
+                    ? matchingCriteria.selectedChampions.preferredChampions.map(c => c.name).join(', ')
+                    : '없음'
+            }
+            </p>
+            <p>
+                비선호 챔피언: {
+                matchingCriteria.selectedChampions.bannedChampions.length > 0
+                    ? matchingCriteria.selectedChampions.bannedChampions.map(c => c.name).join(', ')
+                    : '없음'
+            }
+            </p>
+            <p>원하는 상대의 플레이 스타일: {matchingCriteria.userPreferenceText}</p>
+        </Stats>
     </Container>
   );
 };
diff --git a/frontend/ezgg/src/hooks/useWebSocket.js b/frontend/ezgg/src/hooks/useWebSocket.js
index 2adb36f0..924b2b3b 100644
--- a/frontend/ezgg/src/hooks/useWebSocket.js
+++ b/frontend/ezgg/src/hooks/useWebSocket.js
@@ -1,8 +1,30 @@
+// useWebSocket.js
+// -----------------------------------------------------------------------------
+// STOMP WebSocket 커스텀 훅
+// - UI에서 사용하는 ViewModel(criteria)을 DTO로 변환 후 서버로 전송
+// - onMessage/onConnect/onDisconnect/onError 콜백 지원
+// -----------------------------------------------------------------------------
 import {useCallback, useRef, useState} from 'react';
 import SockJS from 'sockjs-client';
 import { Stomp } from '@stomp/stompjs';
 import api, { tokenUtils } from '../utils/api';
 
+// ---------------- ViewModel → DTO 변환기 --------------------------------------
+/**
+ * 매칭 조건 ViewModel을 DTO로 변환하는 함수.
+ *  - 챔피언 객체 → id 문자열 배열로 축소
+ *  - 불필요 필드는 제거
+ */
+const criteriaToDTO = (vm) => ({
+  wantLine: vm.wantLine,
+  userPreferenceText: vm.userPreferenceText,
+  selectedChampions: {
+    preferredChampions: vm.selectedChampions?.preferredChampions?.map((c) => c.id) ?? [],
+    bannedChampions: vm.selectedChampions?.bannedChampions?.map((c) => c.id) ?? [],
+  }
+});
+
+// ---------------- 커스텀 훅 -----------------------------------------------------
 export const useWebSocket = ({onMessage, onConnect, onDisconnect, onError}) => {
     const stompClient = useRef(null);
     const [isConnected, setIsConnected] = useState(false);
@@ -25,6 +47,7 @@ export const useWebSocket = ({onMessage, onConnect, onDisconnect, onError}) => {
       }
     }
 
+    /** STOMP 서버 연결 함수 */
     const connect = useCallback(async(onConnectedCallback) => {
       if (stompClient.current && stompClient.current.connected) {
         // 이미 연결된 경우 바로 콜백 실행
@@ -62,8 +85,9 @@ export const useWebSocket = ({onMessage, onConnect, onDisconnect, onError}) => {
             onMessage(response);
           });
 
+          // 에러 구독
           stompClient.current.subscribe(`/user/queue/error`, (message) => {
-            onError(message.body);
+              onError(message.body);
           });
 
           if (onConnect) onConnect();
@@ -77,6 +101,7 @@ export const useWebSocket = ({onMessage, onConnect, onDisconnect, onError}) => {
       );
     }, [onConnect, onMessage, onDisconnect, onError]);
 
+    /** 연결 해제 함수 */
     const disconnect = useCallback(() => {
         if (stompClient.current) {
             stompClient.current.disconnect(() => {
@@ -86,29 +111,27 @@ export const useWebSocket = ({onMessage, onConnect, onDisconnect, onError}) => {
         }
     }, [onDisconnect]);
 
-    const sendMatchingRequest = useCallback(async(payload) => {
-        // 필요한 데이터만 추출하여 최적화된 페이로드 생성
-        const optimizedPayload = {
-            wantLine: payload.wantLine,
-            selectedChampions: {
-                preferredChampions: payload.selectedChampions?.preferredChampions?.map(champ => champ.id) || [],
-                bannedChampions: payload.selectedChampions?.bannedChampions?.map(champ => champ.id) || []
-            }
-        };
+    /**
+     * 매칭 요청 전송 함수.
+     *  - ViewModel → DTO 변환 후 JSON 직렬화하여 전송
+     */
+    const sendMatchingRequest = useCallback(async(criteriaVM) => {
+      const dtoPayload = criteriaToDTO(criteriaVM);
+      const json = JSON.stringify(dtoPayload);
 
-        if (!stompClient.current?.connected) {
+        if (!stompClient.current?.connected) {// 연결이 없으면 먼저 connect 후 전송
             // 연결 전에 토큰 유효성 검증
             const isTokenValid = await validateToken();
             if (!isTokenValid) {
-              if (onError) onError('인증이 만료되었습니다');
-              return;
+                if (onError) onError('인증이 만료되었습니다');
+                return;
             }
             connect(() => { // 연결 보장 후 전송
-                stompClient.current.send('/app/matching/start', {}, JSON.stringify(optimizedPayload));
+                stompClient.current.send('/app/matching/start', {}, json);
             });
             return;
         }
-        stompClient.current.send('/app/matching/start', {}, JSON.stringify(optimizedPayload));
+        stompClient.current.send('/app/matching/start', {}, json);
     }, [connect]);
 
     return {connect, disconnect, sendMatchingRequest, isConnected};
diff --git a/frontend/ezgg/src/utils/initialStates.js b/frontend/ezgg/src/utils/initialStates.js
index b2f1ee68..f8a21c82 100644
--- a/frontend/ezgg/src/utils/initialStates.js
+++ b/frontend/ezgg/src/utils/initialStates.js
@@ -3,5 +3,6 @@ export const getInitialCriteria = () => ({
   selectedChampions: {
     preferredChampions: [],
     bannedChampions: []
-  }
+  },
+  userPreferenceText : ''
 });


Reviews:
user: login: llama-w
body: 고생하셨습니다. 주석까지 달아주셔서 감사합니다 👍
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T11:05:51Z
user: login: kdh-92
body: 주석이 있으니 처음에 이해하기 좋네요 :) 고생하셨습니다
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:05:52Z
user: login: chanchangood
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T06:16:55Z
user: login: chanchangood
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:02:00Z
user: login: chanchangood
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:02:38Z
user: login: chanchangood
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:03:11Z
user: login: CHOIIS829
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:05:29Z
user: login: KSJ-0
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:10:38Z

Review comments:
path: frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx
user: login: kdh-92
body: @chanchangood @W-llama @CHOIIS829 @KSJ-0 
별개의 이야기일 수 있는데 원래 js(ES5), jsx 등에서 들여쓰기는 2칸 사용했던걸로 기억하는데, 2칸 할건지 4칸 할건지 정하고 eslint 같은거 적용했으면 좋겠습니다.

(jsx 할때마다 2칸을 해야할지 4칸을 해야할지 멘붕이에요 @_@)
created_at: 2025-05-15T13:56:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,9 +18,9 @@
 public class PreferredPartnerParsingDto {
 
 	private WantLine wantLine;
path: backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java
user: login: kdh-92
body: 여기도 @JsonProperty 줘서 통일시키면 어떨까요?.?
created_at: 2025-05-15T13:57:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,28 +48,51 @@ const handleLineSelect = (type, line, matchingCriteria, setMatchingCriteria) =>
   });
 };
 
+/* ------------- 메인 컴포넌트 ---------------------------------------------------------- */
+
+/**
+ * SectionSelector 컴포넌트
+ * ---------------------------------------------------------------------------
+ * 라인 선택 UI 혹은 챔피언 선택 UI를 렌더링하는 함수형 컴포넌트.
+ *
+ * @prop {object}   matchingCriteria          - 상위 컴포넌트의 상태 객체
+ * @prop {Function} setMatchingCriteria       - 상태 변경 함수(상태를 불변성 지켜 갱신)
+
+ * @prop {'line'|'champion'|'userPreferenceText'} type - 라인 선택 모드 or 챔피언 선택 모드 or 텍스트 입력 모드
+ * @prop {'my'|'partner'|'preferred'|'banned'} kind - 세부 유형(라인: my/partner, 챔피언: preferred/banned)
+
+ * @prop {string}   selectedValue             - (라인 모드) 현재 선택된 값
+ * @prop {string}   disabledValue             - (라인 모드) 클릭 불가능한 값(상대가 이미 선택한 라인)
+ */
 const SectionSelector = ({
                            matchingCriteria,
                            setMatchingCriteria,
-                           type, // 'line' 또는 'champion'
-                           kind, // 'my'/'partner' 또는 'preferred'/'banned'
+                           type, // 'line' | 'champion' | 'userPreferenceText'
+                           kind, // 'my' | 'partner' | 'preferred' | 'banned'
                            selectedValue,
                            disabledValue,
-                           children
+                           children // 향후 확장용 (현재 미사용)
path: frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx
user: login: kdh-92
body: 위에 주석이 있어서 이 부분에는 주석이 없어도 될 것 같습니다 :)
created_at: 2025-05-15T13:59:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,34 @@
+// useWebSocket.js
+// -----------------------------------------------------------------------------
+// STOMP WebSocket 커스텀 훅
+// - UI에서 사용하는 ViewModel(criteria)을 DTO로 변환 후 서버로 전송
+// - onMessage/onConnect/onDisconnect/onError 콜백 지원
+// -----------------------------------------------------------------------------
 import {useCallback, useRef, useState} from 'react';
 import SockJS from 'sockjs-client';
 import {Stomp} from '@stomp/stompjs';
 
+// ---------------- ViewModel → DTO 변환기 --------------------------------------
+/**
+ * 매칭 조건 ViewModel을 DTO로 변환하는 함수.
+ *  - 챔피언 객체 → id 문자열 배열로 축소
+ *  - 불필요 필드는 제거
+ */
+export const criteriaToDTO = (vm) => ({
path: frontend/ezgg/src/hooks/useWebSocket.js
user: login: kdh-92
body: useWebsocket에서만 사용되는 함수이면 export를 제거하는게 맞을 것 같습니다 :)
(재사용성이 있다면 utils에 별도 js로 추가해서 export하고 재사용되는 부분에서 사용하는 방식이 좋을 것 같습니다)
created_at: 2025-05-15T14:05:07Z
author_association: CONTRIBUTOR
path: frontend/ezgg/src/components/DuoFinder/DuoFinderForm.jsx
user: login: chanchangood
body: 월요일 회의 안건에 올려놨습니다!
created_at: 2025-05-16T06:16:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,9 +18,9 @@
 public class PreferredPartnerParsingDto {
 
 	private WantLine wantLine;
path: backend/ezgg/src/main/java/com/matching/ezgg/domain/matching/dto/PreferredPartnerParsingDto.java
user: login: chanchangood
body: 안정성 향상과 통일감을 위해 추가했습니다!
created_at: 2025-05-16T07:02:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,28 +48,51 @@ const handleLineSelect = (type, line, matchingCriteria, setMatchingCriteria) =>
   });
 };
 
+/* ------------- 메인 컴포넌트 ---------------------------------------------------------- */
+
+/**
+ * SectionSelector 컴포넌트
+ * ---------------------------------------------------------------------------
+ * 라인 선택 UI 혹은 챔피언 선택 UI를 렌더링하는 함수형 컴포넌트.
+ *
+ * @prop {object}   matchingCriteria          - 상위 컴포넌트의 상태 객체
+ * @prop {Function} setMatchingCriteria       - 상태 변경 함수(상태를 불변성 지켜 갱신)
+
+ * @prop {'line'|'champion'|'userPreferenceText'} type - 라인 선택 모드 or 챔피언 선택 모드 or 텍스트 입력 모드
+ * @prop {'my'|'partner'|'preferred'|'banned'} kind - 세부 유형(라인: my/partner, 챔피언: preferred/banned)
+
+ * @prop {string}   selectedValue             - (라인 모드) 현재 선택된 값
+ * @prop {string}   disabledValue             - (라인 모드) 클릭 불가능한 값(상대가 이미 선택한 라인)
+ */
 const SectionSelector = ({
                            matchingCriteria,
                            setMatchingCriteria,
-                           type, // 'line' 또는 'champion'
-                           kind, // 'my'/'partner' 또는 'preferred'/'banned'
+                           type, // 'line' | 'champion' | 'userPreferenceText'
+                           kind, // 'my' | 'partner' | 'preferred' | 'banned'
                            selectedValue,
                            disabledValue,
-                           children
+                           children // 향후 확장용 (현재 미사용)
path: frontend/ezgg/src/components/DuoFinder/SectionSelector.jsx
user: login: chanchangood
body: 삭제 했습니다!
created_at: 2025-05-16T07:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,34 @@
+// useWebSocket.js
+// -----------------------------------------------------------------------------
+// STOMP WebSocket 커스텀 훅
+// - UI에서 사용하는 ViewModel(criteria)을 DTO로 변환 후 서버로 전송
+// - onMessage/onConnect/onDisconnect/onError 콜백 지원
+// -----------------------------------------------------------------------------
 import {useCallback, useRef, useState} from 'react';
 import SockJS from 'sockjs-client';
 import {Stomp} from '@stomp/stompjs';
 
+// ---------------- ViewModel → DTO 변환기 --------------------------------------
+/**
+ * 매칭 조건 ViewModel을 DTO로 변환하는 함수.
+ *  - 챔피언 객체 → id 문자열 배열로 축소
+ *  - 불필요 필드는 제거
+ */
+export const criteriaToDTO = (vm) => ({
path: frontend/ezgg/src/hooks/useWebSocket.js
user: login: chanchangood
body: 앞으로 계획된 내용중에 재사용될 일이 없을 것 같아 export문을 삭제 했습니다!
created_at: 2025-05-16T07:03:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: kdh-92
created_at: 2025-05-15T13:51:03Z
body: @KSJ-0 커밋 메세지는 이슈에 맞춰 feat: #40 - 내용으로 가는걸까요? 아니면 찬우님이 이 브런치에 작업하신 것처럼 상황에 맞는 feat, style, refactor 로 가는게 맞는걸까요?
author_association: CONTRIBUTOR
user: login: KSJ-0
created_at: 2025-05-15T15:03:51Z
body: > @KSJ-0 커밋 메세지는 이슈에 맞춰 feat: #40 - 내용으로 가는걸까요? 아니면 찬우님이 이 브런치에 작업하신 것처럼 상황에 맞는 feat, style, refactor 로 가는게 맞는걸까요?

#1 이슈 내 사진처럼 커밋 메세지는 상황에 맞춰 태그 바꿔서 사용하시면 됩니다
author_association: CONTRIBUTOR
user: login: kdh-92
created_at: 2025-05-15T15:41:36Z
body: > #1 이슈 내 사진처럼 커밋 메세지는 상황에 맞춰 태그 바꿔서 사용하시면 됩니다

무조건 issue 태그 맞춰써야한는 줄 알았네요! 감사합니다 :)
author_association: CONTRIBUTOR