Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 27 additions & 20 deletions apps/farminglog/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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);
Expand Down Expand Up @@ -64,6 +66,7 @@ export default function Header() {
<S.ProfileContainer
$isMobile={isMobile}
onClick={(e) => {
if (isLimited) return; // 제한 모드에서는 팝업 비활성화
e.stopPropagation();
setProfilePopupOpen(true);
}}
Expand All @@ -75,10 +78,12 @@ export default function Header() {
/>
<S.ProfileName $isMobile={isMobile}>{name || ""}</S.ProfileName>
</S.ProfileContainer>
<S.RecordCount $isMobile={isMobile} $isTablet={isTablet}>
<span className="seed-text">내 씨앗</span>
<span className="seed-count">{totalSeed ?? 0}</span>
</S.RecordCount>
{!isLimited && (
<S.RecordCount $isMobile={isMobile} $isTablet={isTablet}>
<span className="seed-text">내 씨앗</span>
<span className="seed-count">{totalSeed ?? 0}</span>
</S.RecordCount>
)}
</S.ProfileAndSeedContainer>
);

Expand Down Expand Up @@ -171,19 +176,21 @@ export default function Header() {
</S.HeaderContainer>

{/* 프로필 팝업 */}
<Popup
isOpen={isProfilePopupOpen}
onClose={() => setProfilePopupOpen(false)}
variant="MYPAGE"
userName={user?.name}
generationAndPart={
user?.generation && user?.track
? `${user.generation}기 ${convertTrackToString(user.track)}`
: "기수 정보 없음"
}
profileImg={user?.profileImageUrl}
hasLogout={true}
/>
{!isLimited && (
<Popup
isOpen={isProfilePopupOpen}
onClose={() => setProfilePopupOpen(false)}
variant="MYPAGE"
userName={user?.name}
generationAndPart={
user?.generation && user?.track
? `${user.generation}기 ${convertTrackToString(user.track)}`
: "기수 정보 없음"
}
profileImg={user?.profileImageUrl}
hasLogout={true}
/>
)}
</>
);
}
129 changes: 127 additions & 2 deletions apps/farminglog/src/pages/auth/components/StepStart.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<string | null>(null);
const { post } = usePublicApi();

const type = searchParams.get('type') as 'KAKAO' | 'GOOGLE' | null;

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -107,6 +155,83 @@ const handleVerifyClick = () => {
/>
</S.LinkWrapper>
</S.GapWrapper>

{/* 뷰어 전용 로그인 토글 */}
<div style={{ marginTop: isMobile ? '16px' : '24px' }}>
<button
style={{
background: 'transparent',
border: 'none',
color: '#666',
textDecoration: 'underline',
cursor: 'pointer'
}}
onClick={() => setShowViewerLogin((v) => !v)}
>
SW융합 교육원 뷰어 로그인
</button>
</div>

{showViewerLogin && (
<div style={{
marginTop: isMobile ? '12px' : '16px',
marginBottom: isMobile ? '12px' : '16px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '8px'
}}>
<input
placeholder="ID"
value={viewerId}
onChange={(e) => 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'
}}
/>
<input
placeholder="비밀번호"
type="password"
value={viewerPw}
onChange={(e) => 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 && (
<div style={{ color: '#ff4d4f', fontSize: isMobile ? '12px' : '13px' }}>
{viewerError}
</div>
)}
<button
onClick={handleViewerLogin}
style={{
marginTop: '4px',
width: isMobile ? '175px' : '190px',
height: isMobile ? '36px' : '40px',
backgroundColor: '#29d4a7',
color: '#fff',
border: 'none',
borderRadius: '999px',
cursor: 'pointer',
fontFamily: 'Pretendard Variable',
fontWeight: 600
}}
>
뷰어로 시작하기
</button>
</div>
)}
</S.Container>
);
}
15 changes: 15 additions & 0 deletions apps/farminglog/src/pages/home/Harvest/harvest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,7 @@ const { data: todaySeed, refetch } = useTodaySeedQuery();
const [isInfoOpen, setInfoOpen] = useState(false);
const [isAlready, setIsAlready] = useState(false);
const [showAnimationAfterModal, setShowAnimationAfterModal] = useState<number | null>(null);
const [isLimitedPopup, setIsLimitedPopup] = useState(false);

const buttonRefs = [
useRef<HTMLDivElement>(null),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -257,6 +264,14 @@ const { data: todaySeed, refetch } = useTodaySeedQuery();
subMessage="내일 다시 와주세요!"
confirmLabel="확인"
/>
<Popup
isOpen={isLimitedPopup}
onClose={() => setIsLimitedPopup(false)}
variant="MESSAGE"
mainMessage="제한 계정은 이용할 수 없습니다."
subMessage="관리자에게 문의해주세요."
confirmLabel="확인"
/>
</>
);
}
15 changes: 6 additions & 9 deletions packages/api/hooks/usePrivateApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@ export function usePrivateApi() {
}

try {
const authHeader = tokens ? { Authorization: `Bearer ${tokens.accessToken}` } : {};
const response = await apiConfig.request<ApiResponse<T>>({
url: uri,
method: options.method,
data: options.json,
params: options.searchParams,
headers: {
Authorization: tokens ? `Bearer ${tokens.accessToken}` : "",
},
headers: authHeader,
});

return response.data;
Expand All @@ -89,20 +88,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]
Expand Down
1 change: 1 addition & 0 deletions packages/auth/stores/useAuthStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 17 additions & 2 deletions packages/router/protectedLoader.tsx
Original file line number Diff line number Diff line change
@@ -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}`);
}
}

Expand Down