diff --git a/build.gradle b/build.gradle index 302de1e..061391c 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ configurations { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } } dependencies { @@ -35,6 +36,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'me.paulschwarz:spring-dotenv:4.0.0' implementation 'org.jsoup:jsoup:1.17.2' + implementation platform('org.springframework.ai:spring-ai-bom:1.1.2') + implementation 'org.springframework.ai:spring-ai-starter-model-openai' + implementation 'org.springframework.ai:spring-ai-starter-model-google-genai' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' diff --git a/docs/Backend/01. backend-convention.md b/docs/Backend/01. backend-convention.md index 53e4523..d6e9282 100644 --- a/docs/Backend/01. backend-convention.md +++ b/docs/Backend/01. backend-convention.md @@ -25,11 +25,16 @@ ## 5) 도메인/에러 규칙 - 도메인 객체 생성은 `@Builder`를 사용하고, Builder로 생성한다. -- 에러 코드는 도메인별로 커스텀 `*ErrorCode`를 작성한다. +- 에러 코드는 도메인별로 커스텀 `*ErrorCode` enum을 작성한다. - 에러 코드는 짧고 명확하게 유지한다. +- 각 도메인은 전용 Exception 클래스(`*Exception`)를 작성하여 사용한다. + - 모든 커스텀 예외는 `com.web.SearchWeb.config.exception.BusinessException`을 상속받는다. + - 에러 코드 인터페이스: `com.web.SearchWeb.config.exception.ErrorCode` + - 예시: `linkanalysis` 도메인 → `LinkAnalysisException` ## 6) Config 배치 규칙 - Config는 `config` 폴더 하위에 도메인 폴더를 만든 뒤 배치한다. +- 공통 프레임워크 관련 Config(예외, 응답 등)는 `config.exception`, `config.common` 등에 배치한다. - 예시: 아이템 도메인 Config -> `config/item/ItemConfig.java` ## 7) 머지 기준 diff --git a/frontend/src/app/my-links/page.tsx b/frontend/src/app/my-links/page.tsx index da841ee..abd236f 100644 --- a/frontend/src/app/my-links/page.tsx +++ b/frontend/src/app/my-links/page.tsx @@ -188,7 +188,7 @@ export default function MyLinksPage() { className="bg-white dark:bg-card-dark rounded-lg p-2.5 border border-gray-100 dark:border-gray-800 hover:shadow-sm hover:border-purple-200 dark:hover:border-purple-900/50 transition-all duration-300 group cursor-pointer h-[90px] flex flex-col justify-between focus:ring-1 focus:ring-purple-300 outline-none hover:bg-purple-50/50 dark:hover:bg-purple-900/10" >
-
+
folder_open
@@ -267,37 +425,111 @@ export function SaveLinkDialog() { folder
- {/* AI Recommended Badge */} - - smart_toy - AI Recommended - + {analyzeLinkMutation.isSuccess && analyzeLinkMutation.data?.suggestedFolder && ( + + smart_toy + AI Recommended + + )}
- {folders?.slice(0, 6).map((folder) => { - const isActive = selectedFolderId === folder.memberFolderId; + {displayFolders.map((folder) => { + const isPending = folder.memberFolderId === PENDING_FOLDER_SENTINEL_ID; + const isActive = isPending + ? !!pendingNewFolderName // pending 폴더는 존재 자체가 선택 상태 + : selectedFolderId === folder.memberFolderId; return ( -
setSelectedFolderId(isActive ? null : folder.memberFolderId)} - className={`${styles.folderTile} ${isActive ? styles.folderTileActive : ''} group`} - > - folder +
+ ); })}
-
-
- Browse all folders... - expand_more -
+
+ + + {/* 폴더 브라우저 드롭다운 */} + {openFolderBrowser && ( +
+
+ folder + All Folders ({folders?.length ?? 0}) +
+ + {folders?.map((folder) => { + const isActive = selectedFolderId === folder.memberFolderId; + return ( + + ); + })} +
+ )}
-
@@ -318,22 +550,38 @@ export function SaveLinkDialog() { 2. '선택하지 않은' 기존 태그들은 앞의 8개까지만 "추천"으로 보여줍니다. 3. 나머지는 'Add tag' 버튼을 통해 검색해서 찾도록 유도합니다. */} + {/* AI 추천 태그 (새 태그, isExisting: false) 먼저 표시 */} + {selectedTags + .filter(tag => aiSuggestedTags.has(tag) && !existingTagsList.includes(tag)) + .map((tag) => ( + + ))} + {/* 기존 태그 목록 */} {existingTagsList - .filter(tag => + .filter(tag => selectedTags.includes(tag) || // 선택된 태그거나 existingTagsList.indexOf(tag) < 8 // 상위 8개인 경우만 노출 ) .map((tag) => { const isSelected = selectedTags.includes(tag); - + return ( -
toggleTagSelection(tag)} className={`${styles.tagChip} ${isSelected ? styles.tagChipSelected : styles.tagChipExisting} cursor-pointer`} > {tag} -
+ ); })} @@ -442,7 +690,8 @@ export function SaveLinkDialog() { {existingTagsList .filter(tag => tag.toLowerCase().includes(tagInput.toLowerCase())) .map(tag => ( -
{ @@ -452,7 +701,7 @@ export function SaveLinkDialog() { }} > # {tag} -
+ ))}
@@ -468,10 +717,16 @@ export function SaveLinkDialog() { edit - + {(analyzeLinkMutation.isPending || analyzeLinkMutation.isSuccess) && ( + + )}