Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
49aad74
feat: 레거시 코드 정리 (패키지 주석 처리 및 매퍼 비활성화)
jin2304 Mar 21, 2026
5c7ad55
docs: 아키텍처 및 테스트 시나리오 문서(02, 03) 추가 및 불필요 파일 제외 (.gitignore)
jin2304 Mar 29, 2026
f3386b3
refactor: 사용하지 않는 comment, likes, main, 레거시 인증 파일 정리 및 mapper 백업
jin2304 Mar 29, 2026
9e592e6
feat: Spring Security, JWT 설정 및 Refresh Token 기반 Auth 기능 구축
jin2304 Mar 29, 2026
46fc9e3
refactor: Member 로그인 연동, 소유권 확인(OwnerCheckAspect) 및 폴더 서비스 파라미터명(memb…
jin2304 Mar 29, 2026
8544adb
feat: 프론트엔드 AuthProvider 적용 및 fetchClient JWT 인증 흐름 연동
jin2304 Mar 29, 2026
2cb1273
chore: 레거시 백업 파일 외부 경로로 이동 및 최종 정리
jin2304 Mar 29, 2026
e71bc18
feat(security): JWT 코어 로직 강화 및 보안 필터 예외 처리 구현
jin2304 Apr 1, 2026
8d50921
feat(security): OAuth2 인증 성공/실패 핸들러 고도화 및 쿠키 매핑 수정
jin2304 Apr 1, 2026
bcd182c
feat(auth): 리프레시 토큰 보안 고도화 - 멱등성 보장 및 세션별 버전 관리 기능 개선
jin2304 Apr 1, 2026
7a18ae0
feat(frontend): 자동 토큰 갱신 기능이 통합된 API 클라이언트 및 전역 상태 관리 구현
jin2304 Apr 1, 2026
94b67ad
refactor(frontend): 신규 인증 모델에 기반한 API 모듈 고도화 및 UI 레이아웃 조정
jin2304 Apr 1, 2026
5afa10b
feat(auth): 리프레시 토큰 도메인, 인터페이스 및 예외 코드 누락분 반영
jin2304 Apr 1, 2026
c968254
docs(auth): 리프레시 토큰 고도화 반영 및 테스트 파일 명칭 변경 (auth-test.http)
jin2304 Apr 1, 2026
c8d1dd7
test(auth): 통합 테스트 파일 명칭 변경 및 시나리오 최신화 (auth-test.http)
jin2304 Apr 1, 2026
f24f4f3
fix(auth): 리프레시 토큰 검증 예외를 인증 에러로 매핑
jin2304 Apr 2, 2026
9d6f62b
fix(frontend): OAuth2 인증 경로 리라이트 추가
jin2304 Apr 2, 2026
a565c46
feat: Infrastructure & DB Schema Update for RTR
jin2304 Apr 4, 2026
98bb9c0
feat: Core Auth & JWT Logic Enhancement (RTR)
jin2304 Apr 4, 2026
b4f15f4
feat: Security Configuration & API Filter Chain Optimization
jin2304 Apr 4, 2026
0f18403
feat: Frontend Integration & Auth Flow Support
jin2304 Apr 4, 2026
fbbe02c
docs: Documentation & Automated Test Suite
jin2304 Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,14 @@ out/
.claude/
CLAUDE.md

### OMC ###
.omc/

### HTTP Tests ###
*.http

### Temporary Files ###
.gradle-user/
status.txt
status_utf8.txt
docs/Backend/memo/
Comment on lines +54 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

임시 파일 ignore 범위를 루트로 고정하는 편이 안전합니다.

status.txt/status_utf8.txt처럼 파일명만 적은 패턴은 하위 디렉터리의 동명 파일까지 모두 숨깁니다. 로컬 산출물만 막을 목적이면 루트 기준으로 anchoring 해 두는 편이 부작용이 적습니다.

제안 수정
-.gradle-user/
-status.txt
-status_utf8.txt
-docs/Backend/memo/
+/.gradle-user/
+/status.txt
+/status_utf8.txt
+/docs/Backend/memo/
📝 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
### Temporary Files ###
.gradle-user/
status.txt
status_utf8.txt
docs/Backend/memo/
### Temporary Files ###
/.gradle-user/
/status.txt
/status_utf8.txt
/docs/Backend/memo/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 54 - 58, 현재 .gitignore에 있는 `status.txt`와
`status_utf8.txt` 같은 파일명만 적은 패턴은 하위 디렉터리의 동일 이름 파일까지 모두 무시하므로, 루트 산출물만 무시하려면 파일명
패턴을 루트 기준으로 고정하세요; 즉 `.gitignore`에서 `status.txt`, `status_utf8.txt` 항목을 루트
앵커링(루트 경로 기준) 형태로 변경하고 필요하면 `.gradle-user/`와 `docs/Backend/memo/`도 루트 기준인지 확인해
일관되게 수정해서 의도치 않은 하위 디렉터리 파일 무시를 방지하세요.

7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Comment on lines 28 to 33
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep a template engine for remaining MVC view endpoints

This dependency block removes Thymeleaf, but the app still has MVC controllers that return view names (for example MyPageController#myPage returns "mypage/myPage" and templates are still present under src/main/resources/templates). Without a view engine, those routes will fail at runtime (typically 404/500 on view resolution), so either retain Thymeleaf or migrate those controllers to REST responses.

Useful? React with 👍 / 👎.

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
implementation 'org.jsoup:jsoup:1.17.2'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
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'
Expand Down
897 changes: 897 additions & 0 deletions docs/Backend/02. jwt-oauth2-architecture.md

Large diffs are not rendered by default.

1,282 changes: 1,282 additions & 0 deletions docs/Backend/03. jwt-oauth2-test-scenarios.md

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import type { NextConfig } from "next";

const isDev = process.env.NODE_ENV === 'development';
const backendOrigin = (process.env.NEXT_PUBLIC_BACKEND_ORIGIN || (isDev ? 'http://localhost:8080' : '')).replace(/\/+$/, '');

const nextConfig: NextConfig = {
// [BACKEND_CONNECT] 백엔드 API 프록시 설정 (CORS 해결)
// [BACKEND_CONNECT] 백엔드 API 프록시 설정
// 인증 관련 엔드포인트(/api/auth/*)는 프론트엔드에서 백엔드로 직접 호출한다.
// (buildBackendUrl 참고) Set-Cookie 가 프록시를 거치며 누락되는 문제를 방지하기 위함.
// 아래 rewrite 는 인증 외 일반 API 용이며, Phase 3(Nginx 단일 origin) 도입 시 제거된다.
async rewrites() {
// 운영 환경에서 백엔드 주소가 없는 경우(예: Nginx 단일 오리진 사용) rewrite 생략
if (!backendOrigin) return [];

return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
destination: `${backendOrigin}/api/:path*`,
},
{
source: '/oauth2/:path*',
destination: `${backendOrigin}/oauth2/:path*`,
},
{
source: '/login/oauth2/:path*',
destination: `${backendOrigin}/login/oauth2/:path*`,
},
];
},
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/app/auth/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/store/authStore';

/**
* OAuth 로그인 완료 후 백엔드가 리디렉션하는 콜백 페이지
* - 마운트 시 initialize()를 직접 호출하여 토큰 갱신 및 상태 초기화 진행 (authStore 내부에서 중복 호출 방지)
* - initialize()는 내부적으로 한 번만 실행되도록 싱글톤(Promise) 처리되어 있어 안전함
*/
Comment on lines +7 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주석과 실제 코드 동작이 불일치합니다.

Line 9의 주석에서 "initialize()는 AuthProvider가 단독 호출"이라고 설명하지만, 실제로는 Line 23에서 initialize()를 직접 호출하고 있습니다. 주석을 실제 동작에 맞게 수정해야 합니다.

📝 주석 수정 제안
 /**
  * OAuth 로그인 완료 후 백엔드가 리디렉션하는 콜백 페이지
- * - initialize()는 AuthProvider가 단독 호출 — 여기서는 결과만 구독
+ * - 마운트 시 initialize() 호출 (authStore 내부에서 중복 호출 방지)
  * - 초기화 완료 후 인증 성공 여부에 따라 분기
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/auth/callback/page.tsx` around lines 7 - 11, 주석과 실제 동작이
불일치합니다: 현재 callback page 내에서 initialize()를 직접 호출하고 있으므로 주석에서 "initialize()는
AuthProvider가 단독 호출"이라고 적혀 있는 부분을 삭제하거나 수정해 실제 동작을 반영하세요; 구체적으로 page.tsx의 설명 문구를
"이 페이지에서 initialize()를 호출하여 인증 결과를 처리" 또는 "AuthProvider가 아닌 이 컴포넌트에서
initialize()를 호출" 등으로 바꾸어 initialize(), AuthProvider, 그리고 인증 성공/실패 분기 로직을 정확히
설명하도록 업데이트하세요.

export default function AuthCallbackPage() {
const router = useRouter();
const initialize = useAuthStore((s) => s.initialize);
const isInitializing = useAuthStore((s) => s.isInitializing);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const initialized = useRef(false);

// 컴포넌트 마운트 시 초기화 실행
// - authStore.initialize() 내부에서 이미 진행 중인 Promise가 있다면 재사용하므로 안전함
useEffect(() => {
if (!initialized.current) {
initialize();
initialized.current = true;
}
}, [initialize]);

useEffect(() => {
// 초기화가 끝날 때까지 대기
if (isInitializing) return;

// 인증 성공 여부에 따라 페이지 이동
router.replace(isAuthenticated ? '/my-links' : '/login');
}, [isInitializing, isAuthenticated, router]);

return (
<div className="flex min-h-screen items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<p className="text-sm text-text-sub">로그인 처리 중...</p>
</div>
</div>
);
}
13 changes: 8 additions & 5 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
import { AppLayout } from "@/components/layout/AppLayout";
import { SaveLinkDialog } from "@/components/dialogs/SaveLinkDialog";
import { CreateFolderDialog } from "@/components/dialogs/CreateFolderDialog";
import { AuthProvider } from "@/components/providers/AuthProvider";
import { QueryProvider } from "@/components/providers/QueryProvider";


Expand Down Expand Up @@ -31,11 +32,13 @@ export default function RootLayout({
</head>
<body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main h-screen overflow-hidden flex transition-colors duration-200`}>
<QueryProvider>
<AppLayout>
{children}
<SaveLinkDialog />
<CreateFolderDialog />
</AppLayout>
<AuthProvider>
<AppLayout>
{children}
</AppLayout>
</AuthProvider>
<SaveLinkDialog />
<CreateFolderDialog />
</QueryProvider>
</body>
</html>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from "react";
import NextLink from "next/link";
import { Input } from "@/components/ui/input";
import { buildBackendUrl } from "@/lib/config/backend";

export default function LoginPage() {
return (
Expand Down Expand Up @@ -137,7 +138,8 @@ export default function LoginPage() {
</div>

{/* Google Sign In - Purple Gradient Background */}
<button type="button" className="group relative mb-6 flex h-11 w-full items-center justify-center gap-3 rounded-lg border-t border-white/20 bg-[linear-gradient(135deg,#6d28d9,#8b5cf6)] text-white shadow-md transition-all hover:brightness-110 hover:shadow-primary/25">
{/* 소셜 로그인 시작도 백엔드 직통으로 보내야 OAuth 콜백과 쿠키 발급 주체가 일관된다. */}
<button type="button" onClick={() => { window.location.href = buildBackendUrl('/oauth2/authorization/google'); }} className="group relative mb-6 flex h-11 w-full items-center justify-center gap-3 rounded-lg border-t border-white/20 bg-[linear-gradient(135deg,#6d28d9,#8b5cf6)] text-white shadow-md transition-all hover:brightness-110 hover:shadow-primary/25">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ensure OAuth2 login URL works in relative-path fallback

The login button always navigates to buildBackendUrl('/oauth2/authorization/google'); when NEXT_PUBLIC_BACKEND_ORIGIN is unset (a mode the config explicitly supports), this becomes a relative path on the Next.js app. Since rewrites only proxy /api/:path*, this OAuth2 start route is not forwarded to Spring and can 404, blocking social login in that fallback deployment mode.

Useful? React with 👍 / 👎.

<div className="flex h-6 w-6 items-center justify-center rounded-full bg-white p-1">
<svg className="h-full w-full" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Google Logo</title>
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/app/my-links/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { RightPanel } from '@/components/my-links/RightPanel';
import { useUIStore } from '@/lib/store/uiStore';
import { useFolders } from '@/lib/api/folderApi';
import { useFolderStore } from '@/lib/store/folderStore';
import { TEMP_MEMBER_ID } from '@/lib/auth/currentUser';
import { useAuthStore } from '@/lib/store/authStore';

const PINNED_COLORS = ['bg-blue-600', 'bg-indigo-500', 'bg-teal-500', 'bg-amber-500', 'bg-emerald-600'];

export default function MyLinksPage() {
const { toggleRightPanel } = useUIStore();
const setSelectedFolderId = useFolderStore((s) => s.setSelectedFolderId);

const { data: folders, isLoading, error } = useFolders(TEMP_MEMBER_ID);
const memberId = useAuthStore((s) => s.member?.memberId);
const isAuthInitializing = useAuthStore((s) => s.isInitializing);
const { data: folders, isLoading: isFoldersLoading, error } = useFolders(memberId);

// 인증 정보를 복구 중이거나 아직 폴더 목록을 가져오는 중이면 로딩 상태로 간주
const isLoading = isAuthInitializing || isFoldersLoading;
const pinnedFolders = folders?.slice(0, 5) ?? [];

return (
Expand Down Expand Up @@ -162,8 +166,8 @@ export default function MyLinksPage() {
</div>
)}

{/* ── 빈 상태 ── */}
{folders && folders.length === 0 && (
{/* ── 빈 상태 (로딩이 끝난 후 데이터가 없는 경우) ── */}
{!isLoading && folders && folders.length === 0 && (
<div className="col-span-full flex items-center justify-center py-8">
<div className="flex flex-col items-center gap-1 text-gray-400 text-xs">
<span className="material-symbols-outlined text-2xl">folder_off</span>
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/dialogs/CreateFolderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useUIStore } from '@/lib/store/uiStore';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useState } from 'react';
import { useCreateFolder } from '@/lib/api/folderApi';
import { TEMP_MEMBER_ID } from '@/lib/auth/currentUser';
import { useAuthStore } from '@/lib/store/authStore';

const COLORS = [
{ id: 'purple', base: 'bg-purple-500 hover:ring-purple-500', active: 'ring-purple-500 shadow-[0_0_8px_rgba(168,85,247,0.5)]' },
Expand All @@ -28,6 +28,7 @@ export function CreateFolderDialog() {
const [selectedIcon, setSelectedIcon] = useState('work'); // 선택된 아이콘 이름
const [selectedColor, setSelectedColor] = useState('purple'); // 선택된 색상 ID
const [isPinned, setIsPinned] = useState(true); // 상단 고정 여부
const memberId = useAuthStore((s) => s.member?.memberId);

// 폴더 생성 API 연동 (React Query Mutation)
const createFolderMutation = useCreateFolder();
Expand All @@ -38,12 +39,12 @@ export function CreateFolderDialog() {
*/
const handleCreateFolder = () => {
// 유효성 검사: 이름이 비어있으면 중단
if (!folderName.trim()) return;
if (!folderName.trim() || !memberId) return;

// API 호출
createFolderMutation.mutate(
{
ownerMemberId: TEMP_MEMBER_ID, // 현재는 테스트용 ID 사용
ownerMemberId: memberId,
folderName: folderName.trim(),
},
{
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/components/dialogs/SaveLinkDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useFolders, useCreateFolder } from '@/lib/api/folderApi';
import { useTags, useCreateTag } from '@/lib/api/tagApi';
import { useCreateBookmark, useAnalyzeUrl } from '@/lib/api/bookmarkApi';
import { useAnalyzeLink } from '@/lib/api/linkAnalysisApi';
import { TEMP_MEMBER_ID } from '@/lib/auth/currentUser';
import { useAuthStore } from '@/lib/store/authStore';
import type { LinkAnalysisResponse } from '@/lib/types/linkAnalysis';

// 디자인 시안에서 추출한 커스텀 테마 매핑
Expand Down Expand Up @@ -60,8 +60,9 @@ export function SaveLinkDialog() {
const [pendingNewFolderName, setPendingNewFolderName] = useState<string | null>(null); // AI 추천 새 폴더 (저장 시 생성)

// --- API 연동 (React Query Hooks) ---
const { data: folders } = useFolders(TEMP_MEMBER_ID); // 기존 폴더 목록 조회
const { data: tagsData } = useTags(TEMP_MEMBER_ID); // 기존 태그 목록 조회
const memberId = useAuthStore((s) => s.member?.memberId);
const { data: folders } = useFolders(memberId); // 기존 폴더 목록 조회
const { data: tagsData } = useTags(memberId); // 기존 태그 목록 조회
const createBookmarkMutation = useCreateBookmark(); // 북마크 생성 API 연동
const createFolderMutation = useCreateFolder(); // 폴더 생성 API 연동
const createTagMutation = useCreateTag(); // 태그 생성 API 연동
Expand Down Expand Up @@ -235,12 +236,13 @@ export function SaveLinkDialog() {
const handleSave = () => {
const trimmedUrl = url.trim();
if (!trimmedUrl || !(trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://'))) return;
if (!memberId) return; // 방어 코드: 실제 memberId가 없는 경우 실행 방지

if (pendingNewFolderName) {
// 새 폴더 생성 후 해당 폴더에 북마크 저장
createFolderMutation.mutate(
{
ownerMemberId: TEMP_MEMBER_ID,
ownerMemberId: memberId!,
folderName: pendingNewFolderName,
},
{
Expand All @@ -259,10 +261,10 @@ export function SaveLinkDialog() {
* [핸들러] 새로운 태그를 직접 생성할 때 호출
*/
const handleCreateNewTag = (tagName: string) => {
if (!tagName.trim()) return;
if (!tagName.trim() || !memberId) return;

createTagMutation.mutate(
{ ownerMemberId: TEMP_MEMBER_ID, tagName: tagName.trim() },
{ ownerMemberId: memberId!, tagName: tagName.trim() },
{
onSuccess: () => {
// 태그가 서버에 생성되면, 현재 선택된 태그 목록에도 추가
Expand All @@ -283,7 +285,7 @@ export function SaveLinkDialog() {
if (pendingNewFolderName) {
const pendingVirtual = {
memberFolderId: PENDING_FOLDER_SENTINEL_ID,
ownerMemberId: TEMP_MEMBER_ID,
ownerMemberId: memberId || -1,
parentFolderId: null,
folderName: pendingNewFolderName,
description: null,
Expand Down
36 changes: 28 additions & 8 deletions frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import { cn } from '@/lib/utils';
import { useUIStore } from '@/lib/store/uiStore';
import { useFolders } from '@/lib/api/folderApi';
import { useFolderStore } from '@/lib/store/folderStore';
import { TEMP_MEMBER_ID } from '@/lib/auth/currentUser';
import { useAuthStore } from '@/lib/store/authStore';
import { useRouter } from 'next/navigation';

const PINNED_DOT_COLORS = ['bg-blue-500', 'bg-purple-500', 'bg-teal-500', 'bg-amber-500', 'bg-emerald-500'];

export function Sidebar() {
const pathname = usePathname();
const { data: folders } = useFolders(TEMP_MEMBER_ID);
const router = useRouter();
const logout = useAuthStore((s) => s.logout);
const member = useAuthStore((s) => s.member);
const { data: folders } = useFolders(member?.memberId);
const setSelectedFolderId = useFolderStore((s) => s.setSelectedFolderId);
const pinnedFolders = folders?.slice(0, 5) ?? [];

Expand All @@ -34,13 +38,29 @@ export function Sidebar() {

{/* User Profile Outline */}
<div className="px-3 mb-4">
<div className="flex items-center space-x-2 p-2 bg-white/5 rounded-lg border border-white/10">
<div className="h-7 w-7 rounded-full bg-violet-500/10 border border-violet-500/30 flex items-center justify-center text-violet-300">
<span className="material-symbols-outlined text-[16px]!">person</span>
</div>
<div className="min-w-0">
<p className="font-medium text-xs truncate">My Profile</p>
<div className="flex items-center justify-between p-2 bg-white/5 rounded-lg border border-white/10">
<div className="flex items-center space-x-2 min-w-0">
<div className="h-7 w-7 rounded-full bg-violet-500/10 border border-violet-500/30 flex items-center justify-center text-violet-300">
<span className="material-symbols-outlined text-[16px]!">person</span>
</div>
<div className="min-w-0">
<p className="font-medium text-xs truncate">{member?.name || 'My Profile'}</p>
</div>
</div>
<button
type="button"
onClick={async () => {
try {
await logout();
} finally {
router.push('/login');
}
}}
className="p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded transition-colors"
title="로그아웃"
>
<span className="material-symbols-outlined text-[16px]!">logout</span>
</button>
</div>
</div>

Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/my-links/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useFolders } from '@/lib/api/folderApi';
import { useBookmarks, useDeleteBookmark, useUpdateBookmark } from '@/lib/api/bookmarkApi';
import { useTags } from '@/lib/api/tagApi';
import { useFolderStore } from '@/lib/store/folderStore';
import { TEMP_MEMBER_ID } from '@/lib/auth/currentUser';
import { useAuthStore } from '@/lib/store/authStore';
import type { BookmarkResponse } from '@/lib/types/bookmark';

/**
Expand Down Expand Up @@ -447,10 +447,11 @@ export function RightPanel() {
// --- Central State (Zustand) | 중앙 상태 관리 ---
const { rightPanelOpen } = useUIStore(); // 패널 오픈 여부
const selectedFolderId = useFolderStore((s) => s.selectedFolderId); // 현재 선택된 폴더 ID
const memberId = useAuthStore((s) => s.member?.memberId);

// --- API Data Fetching (React Query) | 서버 데이터 조회 ---
const { data: myFolders, isLoading: isFoldersLoading } = useFolders(TEMP_MEMBER_ID); // 폴더 목록
const { data: tagsData } = useTags(TEMP_MEMBER_ID); // 전체 태그 목록
const { data: myFolders, isLoading: isFoldersLoading } = useFolders(memberId); // 폴더 목록
const { data: tagsData } = useTags(memberId); // 전체 태그 목록

// --- Local UI State | UI 전용 로컬 상태 ---
const [selectedTags, setSelectedTags] = useState<string[]>([]); // 선택된 필터 태그
Expand Down
Loading
Loading