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
26 changes: 26 additions & 0 deletions orval.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineConfig } from 'orval';

// .env.local 파일 로드

export default defineConfig({
api: {
input: {
target: 'https://ht-api.ericpark.shop/swagger-ui/index.html',
},
output: {
target: './src/lib/api/generated.ts',
client: 'axios-functions',
mode: 'tags-split',
schemas: './src/types/api',
override: {
mutator: {
path: './src/lib/api/config.ts',
name: 'apiClient',
},
},
},
hooks: {
afterAllFilesWrite: 'prettier --write',
},
},
});
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"api:generate": "orval",
"api:watch": "orval --watch"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.864.0",
Expand Down Expand Up @@ -49,6 +51,7 @@
"dotenv": "^17.2.1",
"eslint": "^9",
"eslint-config-next": "15.4.5",
"orval": "^7.11.2",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.6",
"typescript": "^5.8.3"
Expand Down
1,348 changes: 1,325 additions & 23 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/svg/logo/instagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/svg/logo/main-logo-gray.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/svg/logo/tiktok.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/svg/logo/youtube.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
3 changes: 3 additions & 0 deletions public/svg/shorts/around-card-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
97 changes: 65 additions & 32 deletions src/app/(pages)/(auth)/auth/kakao/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,72 @@
import { authService } from '@/lib/auth/auth.service';
import { redirect } from 'next/navigation';
'use client';

interface SearchParams {
code?: string;
error?: string;
error_description?: string;
state?: string;
}

export default async function KakaoCallbackPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const params = await searchParams;
const { code, error, error_description } = params;
import { LoadingSpinner } from '@/components/loading-spinner';
import { kakaoLogin } from '@/lib/api/authentication/authentication';
import { getOnboardingStatus } from '@/lib/api/user/user';
import { useAuthStore } from '@/lib/stores/auth-store';
import { redirect, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';

// try {
// const result = await authService.kakaoLogin(code ?? '');
export default function KakaoCallbackPage() {
const searchParams = useSearchParams();
const code = searchParams.get('code');
const [isLoading, setIsLoading] = useState(false);

// // token이 있다고 가정
// const token = result.token;
const setAccessToken = useAuthStore(state => state.setAccessToken);

// const response = await fetch('/api/cookie-set', {
// method: 'POST',
// body: JSON.stringify({ token }),
// });
if (!code) {
console.error('No authorization code received');
redirect('/login?error=no_code');
}
const handleKakaoLogin = async (): Promise<string> => {
const result = await kakaoLogin({
authorizationCode: code,
});
const token = result.accessToken!;
setAccessToken(token);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_ROUTE_URL}/api/cookie-set`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
},
);
if (!response.ok) {
console.error('Failed to set cookie:', response.statusText);
redirect('/login?error=cookie_failed');
}
return token;
};
const handleOnboarding = async () => {
const result = await getOnboardingStatus();
console.log('result', result);
if (!result.termsOfServiceAccepted) {
redirect('/onboarding');
} else {
redirect('/home');
}
};
useEffect(() => {
let isMounted = true;
void (async () => {
void setIsLoading(true);
try {
await handleKakaoLogin();
if (!isMounted) return;
await handleOnboarding();
} finally {
if (!isMounted) return;
void setIsLoading(false);
}
})();

// if (!response.ok) {
// throw new Error('Failed to set cookie');
// }
// redirect('/');
// } catch (error) {
// console.error(error);
// }
return () => {
isMounted = false;
};
}, [code]);

return null;
return <LoadingSpinner />;
}
33 changes: 30 additions & 3 deletions src/app/(pages)/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
'use client';

import { kakaoAuthService } from '@/lib/auth/kakao.service';
import { kakaoAuthService } from '@/services/auth/kakao.service';
import KakaoLogo from '@/public/svg/logo/kakao-logo.svg';
import LoginMainBigLogo from '@/public/svg/logo/login-main-big.svg';
import { useRouter } from 'next/navigation';
import { redirect, useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/stores/auth-store';
import { useEffect } from 'react';

export default function LoginPage() {
const router = useRouter();
const accessToken = useAuthStore(state => state.accessToken);

const handleKakaoLogin = () => {
kakaoAuthService.login();
};
const handleGuestLogin = async () => {
const token = 'guest';
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_ROUTE_URL}/api/cookie-set`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
},
);
if (!response.ok) {
console.error('Failed to set cookie:', response.statusText);
return;
}
router.push('/home');
};
useEffect(() => {
if (accessToken) {
router.push('/home');
}
}, [accessToken]);
return (
<div className='px-6 pt-57 pb-20 min-w-0 w-full h-screen mx-auto login-bg flex flex-col justify-between'>
{/* 헤더 || 로고 영역 */}
Expand All @@ -34,7 +61,7 @@ export default function LoginPage() {

{/* 게스트 로그인 */}
<button
onClick={() => router.push('/home')}
onClick={handleGuestLogin}
className='px-5 py-2 w-full h-14 flex-center bg-orange200 rounded-[6px] text-bodySmall text-orange400'
>
게스트로 로그인
Expand Down
12 changes: 6 additions & 6 deletions src/app/(pages)/(main)/_components/navigation-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import HomeActive from '@/public/svg/nav/home-active.svg';
import HomeInactive from '@/public/svg/nav/home-inactive.svg';
import MakeVideoActive from '@/public/svg/nav/makevideo-active.svg';
import MakeVideoInactive from '@/public/svg/nav/makevideo-inactive.svg';
import ShortsActive from '@/public/svg/nav/shorts-active.svg';
import ShortsInactive from '@/public/svg/nav/shorts-inactive.svg';
import AroundActive from '@/public/svg/nav/around-active.svg';
import AroundInactive from '@/public/svg/nav/around-inactive.svg';
import DashboardActive from '@/public/svg/nav/dashboard-active.svg';
import DashboardInactive from '@/public/svg/nav/dashboard-inactive.svg';
import MyPageActive from '@/public/svg/nav/mypage-active.svg';
Expand Down Expand Up @@ -38,11 +38,11 @@ const navItems: NavItem[] = [
key: 'make-video',
},
{
href: '/shorts',
activeIcon: <ShortsActive />,
inactiveIcon: <ShortsInactive />,
href: '/around',
activeIcon: <AroundActive />,
inactiveIcon: <AroundInactive />,
label: '둘러보기',
key: 'shorts',
key: 'around',
},
{
href: '/dashboard',
Expand Down
47 changes: 47 additions & 0 deletions src/app/(pages)/(main)/around/_components/around-account-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type SnsTab } from '@/hooks/use-sns-tab-query';
import { type SocialAccount } from '@/lib/mockdata/sns-accounts';
import AroundAccountIcon from '@/public/svg/shorts/around-card-arrow.svg';
import { UserCircle } from 'lucide-react';
import Link from 'next/link';
import InstagramIcon from '@/public/svg/logo/instagram.svg';
import YoutubeIcon from '@/public/svg/logo/youtube.svg';
import TiktokIcon from '@/public/svg/logo/tiktok.svg';

const SNS_ICON = {
instagram: <InstagramIcon />,
youtube: <YoutubeIcon />,
tiktok: <TiktokIcon />,
};

export function AroundAccountCard({
account,
sns,
}: {
account: SocialAccount;
sns: SnsTab;
}) {
return (
<Link
href={account.url}
target='_blank'
rel='noopener noreferrer'
className='p-4 flex-1 w-full h-auto flex items-center justify-between rounded-[15px] bg-white000 border border-gray100 shadow-[0_4px_10px_0_rgba(154,159,160,0.15)] hover:bg-orange100 transition-colors duration-200 cursor-pointer'
>
<div className='w-full flex items-center gap-4'>
<span className='relative size-18 aspect-square rounded-full bg-gray400 flex-center'>
<UserCircle className='size-18 text-white000' />
<span className='absolute -bottom-4 -right-4 size-auto aspect-square flex-center'>
{SNS_ICON[sns]}
</span>
</span>
<div className='flex flex-col gap-1 justify-center'>
<h2 className='text-displayMedium text-gray600'>{account.name}</h2>
<p className='text-bodySmall text-gray500'>
팔로워 {account.followers}
</p>
</div>
</div>
<AroundAccountIcon />
</Link>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { useSnsTabQuery } from '@/hooks/use-sns-tab-query';
import {
INSTAGRAM_FOOD_ACCOUNTS,
type SocialAccount,
TIKTOK_FOOD_ACCOUNTS,
YOUTUBE_FOOD_ACCOUNTS,
} from '@/lib/mockdata/sns-accounts';
import { useEffect, useState } from 'react';
import { AroundAccountCard } from './around-account-card';

export function AroundAccountSection() {
const { selectedTab } = useSnsTabQuery();
const [accounts, setAccounts] = useState<SocialAccount[]>([]);
useEffect(() => {
if (selectedTab === 'instagram') {
setAccounts(INSTAGRAM_FOOD_ACCOUNTS);
} else if (selectedTab === 'youtube') {
setAccounts(YOUTUBE_FOOD_ACCOUNTS);
} else if (selectedTab === 'tiktok') {
setAccounts(TIKTOK_FOOD_ACCOUNTS);
}
}, [selectedTab]);

return (
<section className='w-full h-auto flex flex-col gap-4'>
{accounts.map(account => (
<AroundAccountCard
key={account.name}
account={account}
sns={selectedTab}
/>
))}
</section>
);
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
'use client';

import { type SnsTab, useSnsTabQuery } from '@/hooks/use-sns-tab-query';
import { cn } from '@/lib/utils/cn';
import { parseAsStringLiteral, useQueryState } from 'nuqs';

const TABS_LABEL = [
{
label: '전체',
value: 'all',
label: '인스타그램',
value: 'instagram',
},
{
label: '내 주변',
value: 'around',
label: '유튜브',
value: 'youtube',
},
{
label: '동종업계',
value: 'same-industry',
label: '틱톡',
value: 'tiktok',
},
];
type ShortsTab = 'all' | 'around' | 'same-industry';

const SHORTS_TAB_PARSER = parseAsStringLiteral<ShortsTab>([
'all',
'around',
'same-industry',
]);

export function ShortsVideoTabs() {
const [selectedTab, setSelectedTab] = useQueryState<ShortsTab>(
'shortsTab',
SHORTS_TAB_PARSER.withDefault('all'),
);
export function AroundSnsTab() {
const { selectedTab, setSelectedTab } = useSnsTabQuery();
return (
<nav className='sticky top-0 z-10 py-4 w-full h-auto flex items-center justify-start gap-4 bg-gray100'>
{TABS_LABEL.map(tab => (
Expand All @@ -41,7 +30,7 @@ export function ShortsVideoTabs() {
selectedTab === tab.value &&
'bg-orange100 border-orange400 text-orange400 hover:bg-orange200',
)}
onClick={() => setSelectedTab(tab.value as ShortsTab)}
onClick={() => setSelectedTab(tab.value as SnsTab)}
>
{tab.label}
</button>
Expand Down
Loading
Loading