Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
5,202 changes: 4,588 additions & 614 deletions api.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions app/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ axiosInstance.interceptors.response.use(
_retry?: boolean;
};

// 401 에러이고, 재시도하지 않은 요청인 경우
if (error.response?.status === 401 && !originalRequest._retry) {
// 400 또는 401 에러이고, 재시도하지 않은 요청인 경우
if ((error.response?.status === 401 || error.response?.status === 400) && !originalRequest._retry) {
originalRequest._retry = true;

if (!isRefreshing) {
Expand Down Expand Up @@ -119,3 +119,6 @@ axiosInstance.interceptors.response.use(
return Promise.reject(error);
},
);

// apiClient alias for backward compatibility
export const apiClient = axiosInstance;
9 changes: 9 additions & 0 deletions app/assets/home-banner/banner-beauty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions app/assets/home-banner/banner-fashion.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 9 additions & 12 deletions app/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,15 @@ export default function SearchBar({ placeholder = "검색", className = "", valu
onChange={(e) => onChange(e.target.value)}
className="w-full h-10 pl-10 pr-10 text-center text-body2 bg-white border border-gray-200 rounded-[8px] placeholder-text-gray3 focus:outline-none focus:border-core-1 transition-colors"
/>
{value && (
<button
type="button"
onClick={() => onChange("")}
className="absolute right-3 flex items-center justify-center w-5 h-5 bg-text-gray4 rounded-full text-white cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
)}
<button
type="button"
onClick={() => onChange("")}
className="absolute right-3 flex items-center justify-center w-6 h-6 cursor-pointer z-10"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 13.62L14.32 15.94C14.4667 16.0867 14.6533 16.16 14.88 16.16C15.1067 16.16 15.2933 16.0867 15.44 15.94C15.5867 15.7933 15.66 15.6067 15.66 15.38C15.66 15.1533 15.5867 14.9667 15.44 14.82L13.12 12.5L15.44 10.18C15.5867 10.0333 15.66 9.84666 15.66 9.62C15.66 9.39333 15.5867 9.20667 15.44 9.06C15.2933 8.91333 15.1067 8.84 14.88 8.84C14.6533 8.84 14.4667 8.91333 14.32 9.06L12 11.38L9.68 9.06C9.53333 8.91333 9.34666 8.84 9.12 8.84C8.89333 8.84 8.70666 8.91333 8.56 9.06C8.41333 9.20667 8.34 9.39333 8.34 9.62C8.34 9.84666 8.41333 10.0333 8.56 10.18L10.88 12.5L8.56 14.82C8.41333 14.9667 8.34 15.1533 8.34 15.38C8.34 15.6067 8.41333 15.7933 8.56 15.94C8.70666 16.0867 8.89333 16.16 9.12 16.16C9.34666 16.16 9.53333 16.0867 9.68 15.94L12 13.62ZM12 20.5C10.8933 20.5 9.85333 20.2899 8.88 19.8696C7.90667 19.4493 7.06 18.8795 6.34 18.16C5.62 17.4405 5.05013 16.5939 4.6304 15.62C4.21067 14.6461 4.00053 13.6061 4 12.5C3.99947 11.3939 4.2096 10.3539 4.6304 9.38C5.0512 8.40613 5.62107 7.55947 6.34 6.84C7.05893 6.12053 7.9056 5.55067 8.88 5.1304C9.8544 4.71013 10.8944 4.5 12 4.5C13.1056 4.5 14.1456 4.71013 15.12 5.1304C16.0944 5.55067 16.9411 6.12053 17.66 6.84C18.3789 7.55947 18.9491 8.40613 19.3704 9.38C19.7917 10.3539 20.0016 11.3939 20 12.5C19.9984 13.6061 19.7883 14.6461 19.3696 15.62C18.9509 16.5939 18.3811 17.4405 17.66 18.16C16.9389 18.8795 16.0923 19.4496 15.12 19.8704C14.1477 20.2912 13.1077 20.5011 12 20.5Z" fill="#D4D4D9" />
</svg>
</button>
</div>
);
}
7 changes: 3 additions & 4 deletions app/components/common/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from 'react-router';
import { NavLink } from 'react-router';

export interface TabItem {
label: string;
Expand All @@ -22,11 +22,10 @@ export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: T
// Mode 1: Link-based Tab
if (tab.path) {
return (
<Link
<NavLink
key={tab.value}
to={tab.path}
className="flex-1 select-none"
activeOptions={{ exact: false }}
>
{({ isActive }) => (
<div
Expand All @@ -39,7 +38,7 @@ export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: T
)}
</div>
)}
</Link>
</NavLink>
);
}

Expand Down
6 changes: 3 additions & 3 deletions app/components/form/FeeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export default function FeeInput({
unit = "원",
}: FeeInputProps) {
return (
<div className="flex items-center flex-1 h-[34px] px-4 gap-[10px] rounded-md border border-core-2 bg-white/80">
<div className="relative flex items-center flex-1 h-[34px] px-4 rounded-md border border-core-2 bg-white/80">
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value.replace(/[^0-9]/g, ""))}
className="flex-1 text-title3 text-text-black placeholder:text-text-gray3 focus:outline-none bg-transparent text-right"
className="flex-1 text-title3 text-text-black placeholder:text-text-gray3 focus:outline-none bg-transparent text-right pr-8"
placeholder={placeholder}
/>
<span className="text-title3 text-text-black shrink-0">{unit}</span>
<span className="absolute right-4 text-title3 text-text-black shrink-0 pointer-events-none">{unit}</span>
</div>
);
}
2 changes: 1 addition & 1 deletion app/data/campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const CAMPAIGN_DATA: Campaign[] = [
},
];

import { apiClient } from "../lib/api-client"; // 캠페인 제안 보기 api
import { apiClient } from "../api/axios";

// 상세 정보 내 태그 아이템 타입
export interface TagItem {
Expand Down
16 changes: 16 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@
background-color: var(--color-bluegray-1);
min-height: 100vh;
overflow-x: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
}

body::-webkit-scrollbar {
display: none;
}

/* 전역 스크롤바 숨기기 */
*::-webkit-scrollbar {
display: none;
}

* {
-ms-overflow-style: none;
scrollbar-width: none;
}
}

Expand Down
121 changes: 0 additions & 121 deletions app/lib/api-client.ts

This file was deleted.

18 changes: 17 additions & 1 deletion app/lib/token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { jwtDecode } from "jwt-decode";
import type { Role } from "~/types/auth";
import type { Role } from "../types/auth";

const ACCESS_TOKEN_KEY = "accessToken";
const REFRESH_TOKEN_KEY = "refreshToken";
Expand Down Expand Up @@ -90,6 +90,22 @@ export const tokenStorage = {
return payload?.providerId || null;
},

/**
* 현재 로그인한 사용자 Email 반환
*/
getEmail: (): string | null => {
const payload = tokenStorage.decodeAccessToken();
return payload?.email || null;
},

/**
* 현재 로그인한 사용자 Name 반환
*/
getName: (): string | null => {
const payload = tokenStorage.decodeAccessToken();
return payload?.name || null;
},

/**
* 토큰이 만료되었는지 확인
*/
Expand Down
2 changes: 2 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ScrollRestoration,
} from "react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "sonner";
import MobileContainer from "./components/layout/MobileContainer";
import "./globals.css";

Expand Down Expand Up @@ -45,6 +46,7 @@ export default function Root() {
<MobileContainer>
<Outlet />
</MobileContainer>
<Toaster position="top-center" richColors />
</QueryClientProvider>
);
}
15 changes: 9 additions & 6 deletions app/routes/auth/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { apiClient } from "../../../lib/api-client";
import axios from "axios";
import { apiClient } from "../../../api/axios";
import { tokenStorage } from "../../../lib/token";

const BASE_URL = import.meta.env.VITE_API_BASE_URL;
import type {
SignupCompleteRequest,
SignupCompleteResponse,
Expand Down Expand Up @@ -31,18 +34,18 @@ export const signup = async (
* Refresh Token을 사용하여 새로운 Access Token을 발급받습니다.
*/
export const refreshToken = async (): Promise<SignupCompleteResponse> => {
const refreshToken = tokenStorage.getRefreshToken();
const currentRefreshToken = tokenStorage.getRefreshToken();

if (!refreshToken) {
if (!currentRefreshToken) {
throw new Error("No refresh token available");
}

const response = await apiClient.post<SignupCompleteResponse>(
"/api/v1/auth/refresh",
const response = await axios.post<SignupCompleteResponse>(
`${BASE_URL}/api/v1/auth/refresh`,
{},
{
headers: {
RefreshToken: `Bearer ${refreshToken}`,
RefreshToken: `Bearer ${currentRefreshToken}`,
},
}
);
Expand Down
15 changes: 11 additions & 4 deletions app/routes/auth/signup/info/signup-info-content.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import Button from "../../../../components/common/Button";
import { FlowNavigation } from "../../components/FlowNavigation";
import { NameSection } from "../components/NameSection";
Expand All @@ -10,6 +11,7 @@ import { GenderSection } from "../components/GenderSection";
import { ContentCategorySection } from "../components/ContentCategorySection";
import { useAuthStore } from "../../../../stores/auth-store";
import { useSignupStore } from "../../../../stores/signupStore";
import { tokenStorage } from "../../../../lib/token";

interface SocialFormData {
name: string;
Expand All @@ -28,14 +30,19 @@ function SignUpInfoContent() {
const me = useAuthStore((state) => state.me);
const { setBasicInfo, setAdditionalInfo } = useSignupStore();

// JWT 토큰에서 이메일 파싱
const emailFromToken = tokenStorage.getEmail();
const nameFromToken = tokenStorage.getName();

// 에러/성공 상태
const [socialNicknameError, setSocialNicknameError] = useState<string | null>(null);
const [socialNicknameSuccess, setSocialNicknameSuccess] = useState<string | null>(null);

// Social form
const socialForm = useForm<SocialFormData>({
defaultValues: {
email: me?.email || "",
email: emailFromToken || me?.email || "",
name: nameFromToken || me?.name || "",
}
});
const socialNicknameValue = useWatch({ control: socialForm.control, name: "nickname" });
Expand Down Expand Up @@ -66,12 +73,12 @@ function SignUpInfoContent() {
const handleNext = () => {
// 필수 입력값 검증
if (!socialNicknameValue || !socialBirthDateValue || !socialGenderValue) {
alert("모든 필수 정보를 입력해주세요.");
toast.warning("모든 필수 정보를 입력해주세요.");
return;
}

if (selectedCategories.length === 0) {
alert("콘텐츠 분야를 하나 이상 선택해주세요.");
toast.warning("콘텐츠 분야를 하나 이상 선택해주세요.");
return;
}

Expand Down Expand Up @@ -126,7 +133,7 @@ function SignUpInfoContent() {
<EmailSection<SocialFormData>
register={socialForm.register}
errors={socialForm.formState.errors}
emailValue={me?.email || ""}
emailValue={emailFromToken || me?.email || ""}
verificationCodeError={null}
onEmailVerify={() => { }}
readOnly
Expand Down
Loading
Loading