From c258383f8d677b5eecf607f819f5894c12d70b9a Mon Sep 17 00:00:00 2001 From: dongmin0204 Date: Mon, 22 Sep 2025 16:24:51 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20header=20=EB=AF=B8=EB=A6=AC=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/hooks/usePrivateApi.ts | 16 +++++++--------- packages/auth/stores/useAuthStore.ts | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/api/hooks/usePrivateApi.ts b/packages/api/hooks/usePrivateApi.ts index 08e3d522..c3a15191 100644 --- a/packages/api/hooks/usePrivateApi.ts +++ b/packages/api/hooks/usePrivateApi.ts @@ -42,6 +42,7 @@ import apiConfig from '../config/apiConfig'; import { ApiResponse, STATUS, Tokens } from '../models/api'; import { getClientSideTokens } from '../utils/getClientSideTokens'; import { useReissueAccessTokenMutation } from "../services/mutation/useReissueAccessTokenMutation"; +import Cookies from 'js-cookie'; export function usePrivateApi() { @@ -60,14 +61,13 @@ export function usePrivateApi() { } try { + const authHeader = tokens ? { Authorization: `Bearer ${tokens.accessToken}` } : {}; const response = await apiConfig.request>({ url: uri, method: options.method, data: options.json, params: options.searchParams, - headers: { - Authorization: tokens ? `Bearer ${tokens.accessToken}` : "", - }, + headers: authHeader, }); return response.data; @@ -89,20 +89,18 @@ export function usePrivateApi() { // 토큰 재발급 실패시 } } - - navigate("/?toast=401"); - throw error; // ✅ 여기서도 원래 에러 던짐 + throw error; // 여기서도 원래 에러 던짐 } if (status === STATUS.NOT_FOUND) { navigate("/404"); - throw error; // ✅ 상태코드 유지 + throw error; // 상태코드 유지 } - throw error; // ✅ 기타 상태 코드 (400 포함) + throw error; // 기타 상태 코드 (400 포함) } - throw error; // ✅ 네트워크 오류 등 + throw error; // 네트워크 오류 등 } }, [navigate, reissueToken] diff --git a/packages/auth/stores/useAuthStore.ts b/packages/auth/stores/useAuthStore.ts index b5a099b6..87cbd80b 100644 --- a/packages/auth/stores/useAuthStore.ts +++ b/packages/auth/stores/useAuthStore.ts @@ -36,6 +36,7 @@ export const useAuthStore = create( setErrorMessage: (msg: string | null) => set({ errorMessage: msg }), setErrorTitle: (title: string | null) => set({ errorTitle: title }), setToken: (token: string) => set({ accessToken: token }), + setTokens: (accessToken: string, _refreshToken: string) => set({ accessToken }), reset: () => set({ step: "start", From fc07d27be91380e283c053e333177efc77539d5e Mon Sep 17 00:00:00 2001 From: dongmin0204 Date: Mon, 22 Sep 2025 16:28:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=9C=B5=ED=95=A9=20=EA=B5=90?= =?UTF-8?q?=EC=9C=A1=EC=9B=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/auth/components/StepStart.tsx | 129 +++++++++++++++++- packages/api/hooks/usePrivateApi.ts | 1 - 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/apps/farminglog/src/pages/auth/components/StepStart.tsx b/apps/farminglog/src/pages/auth/components/StepStart.tsx index 5a4c06cd..95787868 100644 --- a/apps/farminglog/src/pages/auth/components/StepStart.tsx +++ b/apps/farminglog/src/pages/auth/components/StepStart.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import * as S from '../styles/StepStartStyled'; import AuthButton from './AuthButton'; import { useAuthStore } from '@repo/auth/stores/useAuthStore'; @@ -8,13 +8,20 @@ import signIn from '@/assets/Icons/signIn.png'; import { isKakaoInApp, isAndroid, isIOS } from '@/utils/detect'; import { useSearchParams, useNavigate } from 'react-router'; +import Cookies from 'js-cookie'; +import { usePublicApi } from '@repo/api/hooks/usePublicApi'; export default function StepStart() { - const { setStep } = useAuthStore(); + const { setStep, setToken } = useAuthStore(); const { handleLogin } = useSocialLogin(); const { isMobile } = useMediaQueries(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const [showViewerLogin, setShowViewerLogin] = useState(false); + const [viewerId, setViewerId] = useState(''); + const [viewerPw, setViewerPw] = useState(''); + const [viewerError, setViewerError] = useState(null); + const { post } = usePublicApi(); const type = searchParams.get('type') as 'KAKAO' | 'GOOGLE' | null; @@ -51,6 +58,47 @@ export default function StepStart() { } }; + // SW융합 교육원 뷰어역할로 로그인 ( + const handleViewerLogin = async () => { + const validId = import.meta.env.VITE_UNION_VIEWER_ID; + const validPw = import.meta.env.VITE_UNION_VIEWER_PW; + + if (!validId || !validPw) { + setViewerError('뷰어 계정 환경변수(VITE_UNION_VIEWER_ID/PW)가 설정되지 않았습니다.'); + return; + } + + if (viewerId !== validId || viewerPw !== validPw) { + setViewerError('ID 또는 비밀번호가 올바르지 않습니다.'); + return; + } + + try { + // 임시 토큰 발급 (테스트용) + const userId = Number(import.meta.env.VITE_UNION_VIEWER_NUM); + type TokenDTO = { accessToken: string; refreshToken: string }; + const res = await post<{ status: number; data: TokenDTO }>(`/auth/token/${userId}`); + const tokenWrapper = res as unknown as { status?: number; data?: TokenDTO } | { data?: { data?: TokenDTO } }; + const tokenData = (tokenWrapper as unknown as { data?: { data?: TokenDTO } })?.data?.data || (tokenWrapper as unknown as { data?: TokenDTO })?.data; + const accessToken = tokenData?.accessToken as string | undefined; + const refreshToken = tokenData?.refreshToken as string | undefined; + + if (accessToken && refreshToken) { + Cookies.set('refreshToken', refreshToken, { secure: true, sameSite: 'Strict' }); + setToken(accessToken); // Authorization 즉시 활성화 + Cookies.set('limitWrite', 'true', { secure: true, sameSite: 'Strict' }); + } + } catch { + // 토큰 발급 실패해도 뷰어 세션으로 조회만 시도 + } + + // 뷰어 플래그 제거 + + setViewerError(null); + setShowViewerLogin(false); + navigate('/home'); + }; + // step=input 감지해서 자동 인증 단계 진입 useEffect(() => { const step = searchParams.get('step'); @@ -107,6 +155,83 @@ const handleVerifyClick = () => { /> + + {/* 뷰어 전용 로그인 토글 */} +
+ +
+ + {showViewerLogin && ( +
+ setViewerId(e.target.value)} + style={{ + width: isMobile ? '175px' : '190px', + height: isMobile ? '32px' : '36px', + border: '1px solid #ccc', + borderRadius: '6px', + padding: '6px 10px', + fontFamily: 'Pretendard Variable' + }} + /> + setViewerPw(e.target.value)} + style={{ + width: isMobile ? '175px' : '190px', + height: isMobile ? '32px' : '36px', + border: '1px solid #ccc', + borderRadius: '6px', + padding: '6px 10px', + fontFamily: 'Pretendard Variable' + }} + /> + {viewerError && ( +
+ {viewerError} +
+ )} + +
+ )} ); } diff --git a/packages/api/hooks/usePrivateApi.ts b/packages/api/hooks/usePrivateApi.ts index c3a15191..6737dc8e 100644 --- a/packages/api/hooks/usePrivateApi.ts +++ b/packages/api/hooks/usePrivateApi.ts @@ -42,7 +42,6 @@ import apiConfig from '../config/apiConfig'; import { ApiResponse, STATUS, Tokens } from '../models/api'; import { getClientSideTokens } from '../utils/getClientSideTokens'; import { useReissueAccessTokenMutation } from "../services/mutation/useReissueAccessTokenMutation"; -import Cookies from 'js-cookie'; export function usePrivateApi() { From 20b7d79b89351182e45ae3924190de62fc02fcdc Mon Sep 17 00:00:00 2001 From: dongmin0204 Date: Mon, 22 Sep 2025 16:28:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=9C=B5=ED=95=A9=20=EA=B5=90?= =?UTF-8?q?=EC=9C=A1=EC=9B=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Header/Header.tsx | 47 +++++++++++-------- .../src/pages/home/Harvest/harvest.tsx | 15 ++++++ packages/router/protectedLoader.tsx | 19 +++++++- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/apps/farminglog/src/components/Header/Header.tsx b/apps/farminglog/src/components/Header/Header.tsx index 00b40046..52c9d871 100644 --- a/apps/farminglog/src/components/Header/Header.tsx +++ b/apps/farminglog/src/components/Header/Header.tsx @@ -13,6 +13,7 @@ import useMediaQueries from "@/hooks/useMediaQueries"; import Popup from "@/components/Popup/popup"; import { useUserInfoQuery } from "@repo/auth/services/query/useUserInfoQuery"; import { convertTrackToString } from "@/utils/convertTrackToString"; +import Cookies from "js-cookie"; const navItems = [ { label: "홈", path: "/home" }, @@ -32,10 +33,11 @@ export default function Header() { const { isMobile, isTablet } = useMediaQueries(); const { data: user } = useUserInfoQuery(); + const isLimited = Cookies.get("limitWrite") === "true"; - const name = user?.name; - const profileImageUrl = user?.profileImageUrl; - const totalSeed = user?.totalSeed; + const name = isLimited ? "" : user?.name; + const profileImageUrl = isLimited ? undefined : user?.profileImageUrl; + const totalSeed = isLimited ? 0 : user?.totalSeed; const handleNavigation = (path: string) => { navigate(path); @@ -64,6 +66,7 @@ export default function Header() { { + if (isLimited) return; // 제한 모드에서는 팝업 비활성화 e.stopPropagation(); setProfilePopupOpen(true); }} @@ -75,10 +78,12 @@ export default function Header() { /> {name || ""} - - 내 씨앗 - {totalSeed ?? 0} - + {!isLimited && ( + + 내 씨앗 + {totalSeed ?? 0} + + )} ); @@ -171,19 +176,21 @@ export default function Header() { {/* 프로필 팝업 */} - setProfilePopupOpen(false)} - variant="MYPAGE" - userName={user?.name} - generationAndPart={ - user?.generation && user?.track - ? `${user.generation}기 ${convertTrackToString(user.track)}` - : "기수 정보 없음" - } - profileImg={user?.profileImageUrl} - hasLogout={true} - /> + {!isLimited && ( + setProfilePopupOpen(false)} + variant="MYPAGE" + userName={user?.name} + generationAndPart={ + user?.generation && user?.track + ? `${user.generation}기 ${convertTrackToString(user.track)}` + : "기수 정보 없음" + } + profileImg={user?.profileImageUrl} + hasLogout={true} + /> + )} ); } diff --git a/apps/farminglog/src/pages/home/Harvest/harvest.tsx b/apps/farminglog/src/pages/home/Harvest/harvest.tsx index a679010c..b05dcc2e 100644 --- a/apps/farminglog/src/pages/home/Harvest/harvest.tsx +++ b/apps/farminglog/src/pages/home/Harvest/harvest.tsx @@ -9,6 +9,7 @@ import { useAttendMutation } from "../../../services/mutation/useAttendMutation" import { useTodaySeedQuery } from "../../../services/query/useTodaySeedQuery"; import Popup from "@/components/Popup/popup"; import Info from "@/assets/Icons/info.png"; +import Cookies from "js-cookie"; interface StageProps { text: string; @@ -32,6 +33,7 @@ const { data: todaySeed, refetch } = useTodaySeedQuery(); const [isInfoOpen, setInfoOpen] = useState(false); const [isAlready, setIsAlready] = useState(false); const [showAnimationAfterModal, setShowAnimationAfterModal] = useState(null); + const [isLimitedPopup, setIsLimitedPopup] = useState(false); const buttonRefs = [ useRef(null), @@ -77,6 +79,11 @@ const { data: todaySeed, refetch } = useTodaySeedQuery(); }; const handleButtonClick = async (index: number, link?: string) => { + const isLimited = Cookies.get("limitWrite") === "true"; + if (isLimited) { + setIsLimitedPopup(true); + return; + } const isCompleted = todaySeed ? index === 0 ? todaySeed.isAttendance @@ -257,6 +264,14 @@ const { data: todaySeed, refetch } = useTodaySeedQuery(); subMessage="내일 다시 와주세요!" confirmLabel="확인" /> + setIsLimitedPopup(false)} + variant="MESSAGE" + mainMessage="제한 계정은 이용할 수 없습니다." + subMessage="관리자에게 문의해주세요." + confirmLabel="확인" + /> ); } diff --git a/packages/router/protectedLoader.tsx b/packages/router/protectedLoader.tsx index 054aa507..8e17f656 100644 --- a/packages/router/protectedLoader.tsx +++ b/packages/router/protectedLoader.tsx @@ -1,13 +1,28 @@ import { redirect } from "react-router"; import { getClientSideTokens } from "../api/utils/getClientSideTokens"; +import Cookies from "js-cookie"; export const protectedLoader = async ({ request }: { request: Request }) => { if (typeof window !== "undefined") { const tokens = getClientSideTokens(); + const url = new URL(request.url); + const pathname = url.pathname; + + // 제한된 기능(글 작성, 출석/게임 등) 차단 + const isLimited = Cookies.get("limitWrite") === "true"; + const restrictedPaths = new Set([ + "/cheer/write", + "/farminglog/create", + "/farminglog/edit", + "/game", + ]); + + if (isLimited && restrictedPaths.has(pathname)) { + return redirect("/home"); + } if (!tokens?.accessToken) { - const url = new URL(request.url); - return redirect(`/?from=${url.pathname}`); + return redirect(`/?from=${pathname}`); } }