Skip to content
Open
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
19 changes: 19 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import { ManagerWorkerInvitePage } from '@/pages/manager/worker-invite'
import { WorkspaceJoinPage } from '@/pages/user/workspace-join'
import { MyPage } from '@/pages/my'
import { ProfileEditPage } from '@/pages/my/profile'
import { EmailEditPage } from '@/pages/my/profile/email'
import { NicknameEditPage } from '@/pages/my/profile/nickname'
import { PasswordEditPage } from '@/pages/my/profile/password'
import { SocialAccountPage } from '@/pages/my/profile/social'
import { WithdrawPage } from '@/pages/my/withdraw'
import { ErrorPageRoute } from '@/pages/error'
import { MobileLayout } from '@/shared/ui/MobileLayout'
import { MobileLayoutWithDocbar } from '@/shared/ui/MobileLayoutWithDocbar'
Expand Down Expand Up @@ -104,6 +109,20 @@ export function App() {
element={<AppliedStoresPage />}
/>
<Route path={ROUTES.MY.PROFILE} element={<ProfileEditPage />} />
<Route
path={ROUTES.MY.PROFILE_NICKNAME}
element={<NicknameEditPage />}
/>
<Route
path={ROUTES.MY.PROFILE_PASSWORD}
element={<PasswordEditPage />}
/>
<Route path={ROUTES.MY.PROFILE_EMAIL} element={<EmailEditPage />} />
<Route
path={ROUTES.MY.PROFILE_SOCIAL}
element={<SocialAccountPage />}
/>
<Route path={ROUTES.MY.WITHDRAW} element={<WithdrawPage />} />
<Route
path={ROUTES.MANAGER.WORKER_SCHEDULE}
element={<ManagerWorkerScheduleLegacyEntryRedirect />}
Expand Down
63 changes: 7 additions & 56 deletions src/features/store-register/api/workspaceFileUpload.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,15 @@
import axiosInstance from '@/shared/lib/axiosInstance'
import type { CommonApiResponse } from '@/shared/types/common'
import {
uploadAppFile,
type AppFileBucketType,
type AppFileUploadTargetType,
} from '@/shared/api/appFileUpload'

/** POST /app/files — query `targetType` (Swagger) */
export type AppFileUploadTargetType =
| 'USER_PROFILE'
| 'USER_CERTIFICATE'
| 'POSTING'
| 'WORKSPACE'
| 'WORKSPACE_CERTIFICATE'
| 'WORKSPACE_OWN_IDENTITY'
| 'WORKSPACE_WARRANT'
| 'WORKSPACE_REASON_COMMENT'
| 'CHAT_MESSAGE'

export type AppFileBucketType = 'PUBLIC' | 'PRIVATE'

const APP_FILES_PATH = '/app/files'
export { uploadAppFile }
export type { AppFileBucketType, AppFileUploadTargetType }

/** 증빙 서류는 비공개 버킷 권장(백엔드 요구 시 PUBLIC 로 바꿀 수 있음) */
export const WORKSPACE_REGISTRATION_BUCKET: AppFileBucketType = 'PRIVATE'

function extractUploadedFileId(data: unknown): string {
if (typeof data !== 'object' || data === null) {
throw new Error('파일 업로드 응답이 올바르지 않습니다.')
}
const envelope = data as { data?: unknown }
const inner = envelope.data
if (typeof inner === 'object' && inner !== null) {
const o = inner as Record<string, unknown>
if (typeof o.fileId === 'string' && o.fileId.length > 0) return o.fileId
if (typeof o.id === 'string' && o.id.length > 0) return o.id
}
throw new Error('파일 업로드 응답에 파일 ID가 없습니다.')
}

/**
* POST /app/files
* multipart 파트 이름 `file` + query targetType, bucketType
* (Swagger 의 application/json 예시는 실제 업로드와 다를 수 있음)
*/
export async function uploadAppFile(options: {
file: File
targetType: AppFileUploadTargetType
bucketType: AppFileBucketType
}): Promise<string> {
const formData = new FormData()
formData.append('file', options.file)

const response = await axiosInstance.post<
CommonApiResponse<{ fileId?: string; id?: string } | unknown>
>(APP_FILES_PATH, formData, {
params: {
targetType: options.targetType,
bucketType: options.bucketType,
},
})

return extractUploadedFileId(response.data)
}

export type WorkspaceRegistrationAttachmentKind =
| 'CERTIFICATE'
| 'OWN_IDENTITY'
Expand Down
135 changes: 133 additions & 2 deletions src/features/user/me/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,141 @@
import axiosInstance from '@/shared/lib/axiosInstance'
import type { CommonApiResponse } from '@/shared/types/common'
import type {
LinkSocialAccountRequest,
SendEmailVerificationRequest,
SocialAccountStatusDto,
SocialProvider,
UpdateEmailRequest,
UpdateNicknameRequest,
UpdatePasswordRequest,
UpdateProfileImageRequest,
UserMeApiResponse,
UserMeDto,
UserMeScope,
VerifyEmailCodeRequest,
VerifyEmailCodeResponseDto,
} from '@/features/user/me/types/user'

export async function getUserMe(): Promise<UserMeDto> {
const response = await axiosInstance.get<UserMeApiResponse>('/app/users/me')
function getSelfBasePath(scope: UserMeScope): string {
return scope === 'MANAGER' ? '/manager/me' : '/app/users/me'
}

function getSocialBasePath(scope: UserMeScope): string {
return scope === 'MANAGER' ? '/manager/me/social' : '/app/users/social'
}

export async function getUserMe(scope: UserMeScope): Promise<UserMeDto> {
const response = await axiosInstance.get<UserMeApiResponse>(
getSelfBasePath(scope)
)
return response.data.data
}

export async function updateUserNickname(options: {
scope: UserMeScope
nickname: string
}): Promise<void> {
const body: UpdateNicknameRequest = { nickname: options.nickname }
await axiosInstance.put(`${getSelfBasePath(options.scope)}/nickname`, body)
}

export async function updateUserProfileImage(options: {
scope: UserMeScope
fileId: string
}): Promise<void> {
const body: UpdateProfileImageRequest = { fileId: options.fileId }
await axiosInstance.put(
`${getSelfBasePath(options.scope)}/profile-image`,
body
)
}

export async function deleteUserProfileImage(
scope: UserMeScope
): Promise<void> {
await axiosInstance.delete(`${getSelfBasePath(scope)}/profile-image`)
}

export async function updateUserPassword(options: {
scope: UserMeScope
currentPassword?: string
newPassword: string
}): Promise<void> {
const body: UpdatePasswordRequest = {
newPassword: options.newPassword,
...(options.currentPassword?.trim()
? { currentPassword: options.currentPassword }
: {}),
}
await axiosInstance.put(`${getSelfBasePath(options.scope)}/password`, body)
}

export async function sendUserEmailVerification(options: {
scope: UserMeScope
email: string
}): Promise<void> {
const body: SendEmailVerificationRequest = { email: options.email }
await axiosInstance.post(
`${getSelfBasePath(options.scope)}/email/verification/send`,
body
)
}

export async function verifyUserEmailCode(options: {
scope: UserMeScope
email: string
code: string
}): Promise<string> {
const body: VerifyEmailCodeRequest = {
email: options.email,
code: options.code,
}
const response = await axiosInstance.post<
CommonApiResponse<VerifyEmailCodeResponseDto>
>(`${getSelfBasePath(options.scope)}/email/verification`, body)
return response.data.data.sessionId
}

export async function updateUserEmail(options: {
scope: UserMeScope
sessionId: string
}): Promise<void> {
const body: UpdateEmailRequest = { sessionId: options.sessionId }
await axiosInstance.post(`${getSelfBasePath(options.scope)}/email`, body)
}

export async function deleteUserEmail(scope: UserMeScope): Promise<void> {
await axiosInstance.delete(`${getSelfBasePath(scope)}/email`)
}

export async function withdrawUser(scope: UserMeScope): Promise<void> {
await axiosInstance.delete(getSelfBasePath(scope))
}

export async function linkUserSocialAccount(options: {
scope: UserMeScope
request: LinkSocialAccountRequest
}): Promise<void> {
await axiosInstance.post(
`${getSocialBasePath(options.scope)}/link`,
options.request
)
}

export async function unlinkUserSocialAccount(options: {
scope: UserMeScope
provider: SocialProvider
}): Promise<void> {
await axiosInstance.delete(
`${getSocialBasePath(options.scope)}/unlink/${options.provider}`
)
}

export async function getUserSocialStatus(
scope: UserMeScope
): Promise<SocialAccountStatusDto[]> {
const response = await axiosInstance.get<
CommonApiResponse<SocialAccountStatusDto[]>
>(`${getSocialBasePath(scope)}/status`)
return response.data.data
}
48 changes: 48 additions & 0 deletions src/features/user/me/hooks/useChangeNickname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useState } from 'react'
import { getAxiosErrorMessage } from '@/shared/lib/getAxiosErrorMessage'
import { useUpdateNicknameMutation } from './useUserMeMutations'

export function useChangeNickname(currentNickname: string) {
const updateNicknameMutation = useUpdateNicknameMutation()
const [nickname, setNickname] = useState(currentNickname)
const [message, setMessage] = useState('')

const trimmedNickname = nickname.trim()
const canSubmit =
trimmedNickname.length > 0 &&
trimmedNickname.length <= 64 &&
trimmedNickname !== currentNickname &&
!updateNicknameMutation.isPending

const submit = async (): Promise<boolean> => {
setMessage('')
if (!trimmedNickname) {
setMessage('닉네임을 입력해 주세요.')
return false
}
if (trimmedNickname.length > 64) {
setMessage('닉네임은 64자 이하로 입력해 주세요.')
return false
}
if (trimmedNickname === currentNickname) {
setMessage('현재 닉네임과 다른 닉네임을 입력해 주세요.')
return false
}

try {
await updateNicknameMutation.mutateAsync(trimmedNickname)
return true
} catch (error) {
setMessage(getAxiosErrorMessage(error, '닉네임 변경에 실패했습니다.'))
return false
}
}

return {
nickname,
setNickname,
message,
canSubmit,
submit,
}
}
Loading
Loading