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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
]
},
"dependencies": {
"@stomp/stompjs": "^7.3.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.90.21",
"axios": "^1.13.6",
Expand Down
16 changes: 15 additions & 1 deletion src/app/layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { Outlet, useLocation } from 'react-router-dom';

import { redirectToLogin, refreshAccessToken } from '@/shared/lib/auth/refreshSession';
import { getAccessToken, getRefreshToken, isAccessTokenExpired } from '@/shared/lib/auth/token';
import { Header } from '@/widgets/header';

export function AppLayout() {
const location = useLocation();
// ์‚ฐ์ฑ… ํŽ˜์ด์ง€๋Š” ํ—ค๋” ์—†๋Š” ์ „์ฒดํ™”๋ฉด(์ง€๋„) ๋ ˆ์ด์•„์›ƒ
const isFullBleed = location.pathname.startsWith('/walk');

useEffect(() => {
if (!getAccessToken() || !getRefreshToken() || !isAccessTokenExpired()) {
return;
Expand All @@ -21,6 +25,16 @@ export function AppLayout() {
void refreshSession();
}, []);

if (isFullBleed) {
return (
<div className="flex min-h-screen flex-col bg-neutral-50">
<main className="flex-1">
<Outlet />
</main>
</div>
);
}

return (
<div className="flex min-h-screen flex-col bg-neutral-50">
<Header />
Expand Down
9 changes: 9 additions & 0 deletions src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { NotificationSettingsPage } from '@/pages/my/NotificationSettingsPage';
import { PetRegistrationPage } from '@/pages/my/PetRegistrationPage';
import { PetSpecialNotesPage } from '@/pages/my/PetSpecialNotesPage';
import { PetWeightPage } from '@/pages/my/PetWeightPage';
import { WithdrawalPage } from '@/pages/my/WithdrawalPage';
import { NotFoundPage } from '@/pages/not-found/NotFoundPage';
import { WalkPage } from '@/pages/walk/WalkPage';

Expand Down Expand Up @@ -78,6 +79,14 @@ export const router = createBrowserRouter([
</RequireAuth>
),
},
{
path: '/my/withdrawal',
element: (
<RequireAuth>
<WithdrawalPage />
</RequireAuth>
),
},
{
path: '/my/pets/new',
element: (
Expand Down
11 changes: 11 additions & 0 deletions src/features/auth/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { apiClient } from '@/shared/api/axios';

import type {
LogoutResponse,
NicknameCheckResponse,
NotificationUpdateResponse,
RegisterProfileRequest,
Expand Down Expand Up @@ -35,6 +36,16 @@ export async function socialLogin(provider: SocialProvider, code: string): Promi
return { kind: 'LOGIN', data: response.data as SocialLoginSuccess };
}

/**
* ๋กœ๊ทธ์•„์›ƒ (POST /auth/logout)
* - refreshToken ์‚ญ์ œ + accessToken ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ
* - accessToken์€ apiClient ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ Authorization ํ—ค๋”๋กœ ์ž๋™ ์ฒจ๋ถ€
*/
export async function logout(refreshToken: string): Promise<LogoutResponse> {
const response = await apiClient.post<LogoutResponse>('/auth/logout', { refreshToken });
return response.data;
}

/**
* ์ถ”๊ฐ€ ์ •๋ณด ์ž…๋ ฅ โ†’ ๊ฐ€์ž… ์™„๋ฃŒ (PUT /users/me/profile)
* - 202 ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ registrationToken์„ Authorization ํ—ค๋”๋กœ ์ „๋‹ฌ
Expand Down
29 changes: 28 additions & 1 deletion src/features/auth/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { apiClient } from '@/shared/api/axios';

import type { UpdateMyProfileRequest, UpdateMyProfileResponse, UserProfile } from '../model/types';
import type {
UpdateMyProfileRequest,
UpdateMyProfileResponse,
UserProfile,
WithdrawUserResponse,
WithdrawalEmailResponse,
} from '../model/types';

/**
* ๋‚ด ์ •๋ณด ์กฐํšŒ (GET /users/me)
Expand All @@ -19,3 +25,24 @@ export async function updateMyProfile(body: UpdateMyProfileRequest): Promise<Upd
const response = await apiClient.patch<UpdateMyProfileResponse>('/users/me', body);
return response.data;
}

/**
* ํƒˆํ‡ด ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก (POST /users/me/withdrawal/email)
* - ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ € ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐœ์†ก
* - 1๋ถ„ ์ด๋‚ด ์žฌ์š”์ฒญ ์‹œ 429 ์‘๋‹ต
*/
export async function sendWithdrawalEmail(): Promise<WithdrawalEmailResponse> {
const response = await apiClient.post<WithdrawalEmailResponse>('/users/me/withdrawal/email');
return response.data;
}

/**
* ์ตœ์ข… ํšŒ์› ํƒˆํ‡ด (DELETE /users/me)
* - ๋ฉ”์ผ๋กœ ๋ฐ›์€ 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ(authCode)๋กœ ๊ณ„์ • ์‚ญ์ œ
*/
export async function withdrawUser(authCode: string): Promise<WithdrawUserResponse> {
const response = await apiClient.delete<WithdrawUserResponse>('/users/me', {
data: { authCode },
});
return response.data;
}
15 changes: 13 additions & 2 deletions src/features/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export {
export type { AuthErrorPresentation, AuthClientErrorCode, AuthErrorContext } from './lib/authErrorPresentation';
export { AuthLoadingScreen } from './ui/status/AuthLoadingScreen';
export { AuthErrorScreen } from './ui/status/AuthErrorScreen';
export { socialLogin, registerProfile, checkNicknameAvailability, updateNotificationSetting } from './api/auth';
export { getMyProfile, updateMyProfile } from './api/users';
export { socialLogin, logout, registerProfile, checkNicknameAvailability, updateNotificationSetting } from './api/auth';
export { getMyProfile, updateMyProfile, sendWithdrawalEmail, withdrawUser } from './api/users';
export { useCreatePet } from './model/useCreatePet';
export { useCreatePetInvitationCode } from './model/useCreatePetInvitationCode';
export { useCreatePetSpecialNote } from './model/useCreatePetSpecialNote';
Expand All @@ -29,6 +29,9 @@ export { useFamilyApplications } from './model/useFamilyApplications';
export { useFamilyBlockedUsers } from './model/useFamilyBlockedUsers';
export { useFamilyPendingUsers } from './model/useFamilyPendingUsers';
export { useCurrentUser } from './model/useCurrentUser';
export { useLogout } from './model/useLogout';
export { useSendWithdrawalEmail } from './model/useSendWithdrawalEmail';
export { useWithdrawUser } from './model/useWithdrawUser';
export { useApprovePetFamilyRequest } from './model/useApprovePetFamilyRequest';
export { useDeletePetWeight } from './model/useDeletePetWeight';
export { useDeletePetSpecialNote } from './model/useDeletePetSpecialNote';
Expand All @@ -50,6 +53,8 @@ export type {
SocialLoginSuccess,
SocialSignupRequired,
SocialLoginResult,
LogoutRequest,
LogoutResponse,
CreatePetRequest,
CreatePetResponse,
CreatePetSpecialNoteRequest,
Expand Down Expand Up @@ -78,6 +83,9 @@ export type {
NotificationUpdateResponse,
UpdateMyProfileRequest,
UpdateMyProfileResponse,
WithdrawalEmailResponse,
WithdrawUserRequest,
WithdrawUserResponse,
PetDetailResponse,
PetFamilyApprovalAction,
PetFamilyApprovalRequest,
Expand All @@ -102,8 +110,11 @@ export type {
export { getApiErrorMessage, getErrorBodyMessage } from '@/shared/lib/api/errorMessage';
export {
SOCIAL_LOGIN_STATUS_MESSAGES,
LOGOUT_STATUS_MESSAGES,
REGISTER_PROFILE_STATUS_MESSAGES,
NOTIFICATION_SETTING_STATUS_MESSAGES,
PROFILE_UPDATE_STATUS_MESSAGES,
NICKNAME_CHECK_STATUS_MESSAGES,
WITHDRAWAL_EMAIL_STATUS_MESSAGES,
WITHDRAW_USER_STATUS_MESSAGES,
} from './lib/apiErrorMessages';
24 changes: 24 additions & 0 deletions src/features/auth/lib/apiErrorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export const SOCIAL_LOGIN_STATUS_MESSAGES: Partial<Record<number, string>> = {
500: '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
};

/** ๋กœ๊ทธ์•„์›ƒ POST /auth/logout */
export const LOGOUT_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '์ž˜๋ชป๋œ ์š”์ฒญ์ด์—์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
401: '์ธ์ฆ ์ •๋ณด๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์•„์š”. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.',
404: '๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.',
500: '๋กœ๊ทธ์•„์›ƒ์— ์‹คํŒจํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
};

/** ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ PUT /users/me/profile */
export const REGISTER_PROFILE_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '์ž…๋ ฅ ์ •๋ณด๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
Expand All @@ -31,6 +39,22 @@ export const PROFILE_UPDATE_STATUS_MESSAGES: Partial<Record<number, string>> = {
500: 'ํšŒ์›์ •๋ณด ์ˆ˜์ •์— ์‹คํŒจํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
};

/** ํƒˆํ‡ด ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก POST /users/me/withdrawal/email */
export const WITHDRAWAL_EMAIL_STATUS_MESSAGES: Partial<Record<number, string>> = {
401: '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด์—์š”. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.',
404: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.',
429: '์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. (1๋ถ„ ์ด๋‚ด ์žฌ์š”์ฒญ์€ ๋ถˆ๊ฐ€ํ•ด์š”)',
500: '์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก์— ์‹คํŒจํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
};

/** ์ตœ์ข… ํšŒ์› ํƒˆํ‡ด DELETE /users/me */
export const WITHDRAW_USER_STATUS_MESSAGES: Partial<Record<number, string>> = {
400: '์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
401: '์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋˜์—ˆ์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
404: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.',
500: 'ํšŒ์› ํƒˆํ‡ด์— ์‹คํŒจํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
};

/** ๋‹‰๋„ค์ž„ ์ค‘๋ณต ํ™•์ธ GET /users/nickname/check */
export const NICKNAME_CHECK_STATUS_MESSAGES: Partial<Record<number, string>> = {
500: '์ค‘๋ณต ํ™•์ธ์— ์‹คํŒจํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
Expand Down
28 changes: 28 additions & 0 deletions src/features/auth/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export interface TokenReissueResponse {
accessTokenExpiresIn: number;
}

// ---- ๋กœ๊ทธ์•„์›ƒ (POST /auth/logout) ----

export interface LogoutRequest {
/** ์‚ญ์ œํ•  ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ */
refreshToken: string;
}

export interface LogoutResponse {
message: string;
}

// ---- ์†Œ์…œ ๋กœ๊ทธ์ธ (POST /auth/social-login) ----

export interface SocialLoginRequest {
Expand Down Expand Up @@ -408,6 +419,23 @@ export interface UpdateMyProfileResponse {
userCreatedAt: string;
}

// ---- ํšŒ์› ํƒˆํ‡ด ----

/** ํƒˆํ‡ด ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก (POST /users/me/withdrawal/email) */
export interface WithdrawalEmailResponse {
message: string;
}

/** ์ตœ์ข… ํšŒ์› ํƒˆํ‡ด (DELETE /users/me) */
export interface WithdrawUserRequest {
/** ๋ฉ”์ผ๋กœ ๋ฐ›์€ 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ */
authCode: string;
}

export interface WithdrawUserResponse {
message: string;
}

export interface UserProfile {
message: string;
userId?: string;
Expand Down
21 changes: 21 additions & 0 deletions src/features/auth/model/useLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation } from '@tanstack/react-query';

import { logout } from '@/features/auth/api/auth';
import type { LogoutResponse } from '@/features/auth/model/types';
import { getRefreshToken } from '@/shared/lib/auth/token';

/**
* ๋กœ๊ทธ์•„์›ƒ (POST /auth/logout)
* - refreshToken์ด ์—†์œผ๋ฉด ์„œ๋ฒ„ ํ˜ธ์ถœ ์—†์ด ๋กœ์ปฌ ์„ธ์…˜๋งŒ ์ •๋ฆฌํ•˜๋„๋ก null ๋ฐ˜ํ™˜
* - ํ† ํฐ/์บ์‹œ ์ •๋ฆฌยท์ด๋™์€ ํ˜ธ์ถœ ์ธก์—์„œ ์ฒ˜๋ฆฌ
*/
export function useLogout() {
return useMutation<LogoutResponse | null, unknown, void>({
mutationFn: async () => {
const refreshToken = getRefreshToken();
if (!refreshToken) return null;

return logout(refreshToken);
},
});
}
11 changes: 11 additions & 0 deletions src/features/auth/model/useSendWithdrawalEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMutation } from '@tanstack/react-query';

import { sendWithdrawalEmail } from '@/features/auth/api/users';
import type { WithdrawalEmailResponse } from '@/features/auth/model/types';

/** ํƒˆํ‡ด ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก (POST /users/me/withdrawal/email) */
export function useSendWithdrawalEmail() {
return useMutation<WithdrawalEmailResponse, unknown, void>({
mutationFn: () => sendWithdrawalEmail(),
});
}
11 changes: 11 additions & 0 deletions src/features/auth/model/useWithdrawUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMutation } from '@tanstack/react-query';

import { withdrawUser } from '@/features/auth/api/users';
import type { WithdrawUserResponse } from '@/features/auth/model/types';

/** ์ตœ์ข… ํšŒ์› ํƒˆํ‡ด (DELETE /users/me) โ€” ๋ฉ”์ผ๋กœ ๋ฐ›์€ 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ๋กœ ๊ณ„์ • ์‚ญ์ œ */
export function useWithdrawUser() {
return useMutation<WithdrawUserResponse, unknown, string>({
mutationFn: (authCode) => withdrawUser(authCode),
});
}
50 changes: 50 additions & 0 deletions src/features/walk-fence/api/fence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { apiClient } from '@/shared/api/axios';

import type {
CreateFenceRequest,
FenceBoundariesResponse,
FenceBoundaryResponse,
FenceMessageResponse,
FenceStatusResponse,
ToggleFenceRequest,
UpdateFenceRangeRequest,
} from '../model/types';

/** 1. ์šธํƒ€๋ฆฌ ์ƒ์„ฑ */
export async function createFence(payload: CreateFenceRequest): Promise<FenceMessageResponse> {
const response = await apiClient.post<FenceMessageResponse>('/fence/range', payload);
return response.data;
}

/** 2. ํŠน์ • ๋ฐ˜๋ ค๋™๋ฌผ์˜ ์šธํƒ€๋ฆฌ ํ™œ์„ฑํ™” ์ƒํƒœ ์กฐํšŒ */
export async function getFenceStatus(petId: number): Promise<FenceStatusResponse> {
const response = await apiClient.get<FenceStatusResponse>(`/fence/${petId}/status`);
return response.data;
}

/** 3. ์šธํƒ€๋ฆฌ ON/OFF ๋ณ€๊ฒฝ */
export async function toggleFence(fenceId: number, payload: ToggleFenceRequest): Promise<FenceMessageResponse> {
const response = await apiClient.patch<FenceMessageResponse>(`/fence/${fenceId}/toggle`, payload);
return response.data;
}

/** 4. ์šธํƒ€๋ฆฌ ์ด๋ฆ„/์ค‘์‹ฌ/๋ฐ˜๊ฒฝ ์ˆ˜์ • */
export async function updateFenceRange(
fenceId: number,
payload: UpdateFenceRangeRequest,
): Promise<FenceMessageResponse> {
const response = await apiClient.patch<FenceMessageResponse>(`/fence/${fenceId}/range`, payload);
return response.data;
}

/** 5. ์ง€๋„์— ํ‘œ์‹œํ•  ๋‹จ์ผ ์šธํƒ€๋ฆฌ ๊ฒฝ๊ณ„ ์กฐํšŒ */
export async function getFenceBoundary(fenceId: number): Promise<FenceBoundaryResponse> {
const response = await apiClient.get<FenceBoundaryResponse>(`/fence/${fenceId}/boundary`);
return response.data;
}

/** 6. ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์šธํƒ€๋ฆฌ ๊ฒฝ๊ณ„ ๋ชฉ๋ก ์กฐํšŒ */
export async function getFenceBoundaries(): Promise<FenceBoundariesResponse> {
const response = await apiClient.get<FenceBoundariesResponse>('/fence/boundaries');
return response.data;
}
29 changes: 29 additions & 0 deletions src/features/walk-fence/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export {
createFence,
getFenceBoundaries,
getFenceBoundary,
getFenceStatus,
toggleFence,
updateFenceRange,
} from './api/fence';
export { useCreateFence } from './model/useCreateFence';
export { useFenceBoundaries } from './model/useFenceBoundaries';
export { useFenceStatus } from './model/useFenceStatus';
export { useLiveLocation } from './model/useLiveLocation';
export { useToggleFence } from './model/useToggleFence';
export { useUpdateFenceRange } from './model/useUpdateFenceRange';
export { FenceControlPanel } from './ui/FenceControlPanel';
export { WalkMap } from './ui/WalkMap';
export type {
CreateFenceRequest,
FenceBoundariesResponse,
FenceBoundary,
FenceBoundaryResponse,
FenceCenter,
FenceMessageResponse,
FenceStatusResponse,
LiveLocationMessage, // ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์„œ๋ฒ„ ๋ฉ”์„ธ์ง€ ์ „์ฒด ๋ฐ›๊ธฐ
LiveLocationPayload, // ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์„œ๋ฒ„ ๋ฉ”์„ธ์ง€ ์ค‘ payload ๋ถ€๋ถ„
ToggleFenceRequest,
UpdateFenceRangeRequest,
} from './model/types';
Loading
Loading