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
5 changes: 5 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
type="text/javascript"
src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"
></script>
<!-- 네이버 지도 API v3 -->
<script
type="text/javascript"
src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=%VITE_NAVER_MAP_CLIENT_ID%"
></script>
</head>
<body>
<div id="root"></div>
Expand Down
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"firebase": "^12.10.0",
"framer-motion": "^12.38.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.12.0",
Expand Down
10 changes: 10 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { SocialChatPage } from '@/pages/manager/social-chat'
import { LoginPage } from '@/pages/login'
import { KakaoCallbackPage } from '@/pages/oauth/KakaoCallbackPage'
import { JobLookupMapPage } from '@/pages/user/job-lookup-map'
import { JobLookupMapApplyPage } from '@/pages/user/job-lookup-map-apply'
import { JobLookupMapDetailPage } from '@/pages/user/job-lookup-map-detail'
import { SchedulePage } from '@/pages/user/schedule'
import { UserHomePage } from '@/pages/user/home'
import { WorkspaceMembersPage } from '@/pages/user/workspace-members'
Expand Down Expand Up @@ -107,6 +109,14 @@ export function App() {
path={ROUTES.USER.JOB_LOOKUP_MAP}
element={<JobLookupMapPage />}
/>
<Route
path={ROUTES.USER.JOB_LOOKUP_MAP_DETAIL}
element={<JobLookupMapDetailPage />}
/>
<Route
path={ROUTES.USER.JOB_LOOKUP_MAP_APPLY}
element={<JobLookupMapApplyPage />}
/>
<Route
path={ROUTES.USER.HOME}
element={
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/job-lookup-map/Chevrondown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/assets/icons/job-lookup-map/List.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/assets/icons/job-lookup-map/Mappin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/features/auth/ui/KakaoLoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ export function KakaoLoginButton({ redirectFrom }: KakaoLoginButtonProps) {
try {
setIsLoading(true)

const authorizationCode = await requestFreshKakaoAuthorizationCode()
const { authorizationCode, redirectUri } =
await requestFreshKakaoAuthorizationCode(getKakaoOAuthRedirectUri())

await loginSocial(
{
provider: 'KAKAO',
authorizationCode,
redirectUri: getKakaoOAuthRedirectUri(),
redirectUri,
platformType: 'WEB',
},
setAuth,
Expand Down
96 changes: 96 additions & 0 deletions src/features/job-lookup-map/api/posting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import axiosInstance from '@/shared/lib/axiosInstance'
import type { CommonApiResponse } from '@/shared/types/common'
import type {
ApplyPostingRequest,
PostingListResponse,
PostingDetailResponse,
} from '@/features/job-lookup-map/types/posting'

export type FetchPostingsParams = {
pageSize: number
cursor?: string
}

function isCommonApiEnvelope(
value: unknown
): value is CommonApiResponse<unknown> {
return (
value !== null &&
typeof value === 'object' &&
'timestamp' in value &&
'data' in value
)
}

function isPostingDetailResponse(
value: unknown
): value is PostingDetailResponse {
if (value === null || typeof value !== 'object') return false
const record = value as Record<string, unknown>
const workspace = record.workspace
return (
typeof record.id === 'number' &&
typeof record.title === 'string' &&
typeof record.description === 'string' &&
typeof record.payAmount === 'number' &&
typeof record.paymentType === 'string' &&
typeof record.createdAt === 'string' &&
typeof record.scrapped === 'boolean' &&
Array.isArray(record.keywords) &&
Array.isArray(record.schedules) &&
workspace !== null &&
typeof workspace === 'object' &&
typeof (workspace as { id?: unknown }).id === 'number'
)
}

function unwrapPostingDetailBody(body: unknown): PostingDetailResponse {
if (isCommonApiEnvelope(body)) {
if (!isPostingDetailResponse(body.data)) {
throw new Error('공고 상세 응답 형식이 올바르지 않습니다.')
}
return body.data
}

if (isPostingDetailResponse(body)) {
return body
}

throw new Error('공고 상세를 불러오지 못했습니다.')
}

export async function fetchPostings(
params: FetchPostingsParams
): Promise<PostingListResponse> {
const response = await axiosInstance.get<PostingListResponse>(
'/app/postings',
{
params: {
pageSize: params.pageSize,
...(params.cursor !== undefined &&
params.cursor !== '' && { cursor: params.cursor }),
},
}
)
return response.data
}

export async function fetchPostingDetail(
postingId: number
): Promise<PostingDetailResponse> {
const response = await axiosInstance.get<unknown>(
`/app/postings/${postingId}`
)
return unwrapPostingDetailBody(response.data)
}

/** POST /app/postings/apply/{postingId} — 공고 지원 */
export async function applyPosting(
postingId: number,
body: ApplyPostingRequest
): Promise<void> {
await axiosInstance.post<CommonApiResponse<Record<string, never>>>(
`/app/postings/apply/${postingId}`,
body
)
}
103 changes: 103 additions & 0 deletions src/features/job-lookup-map/common/AlbaFindCategoryBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import ChevrondownIcon from '@/assets/icons/job-lookup-map/Chevrondown.svg?react'

export type AlbaFindMode = 'nearby' | 'region'

export type AlbaFindFilterId = 'sort' | 'distance' | 'salary'

type AlbaFindCategoryBarProps = {
mode: AlbaFindMode
onModeChange: (mode: AlbaFindMode) => void
activeFilter: AlbaFindFilterId
onFilterChange: (id: AlbaFindFilterId) => void
}

const NEARBY_FILTER_ITEMS: { id: AlbaFindFilterId; label: string }[] = [
{ id: 'sort', label: '최신순' },
{ id: 'distance', label: '거리' },
{ id: 'salary', label: '급여' },
]

const REGION_FILTER_ITEMS: { id: AlbaFindFilterId; label: string }[] = [
{ id: 'sort', label: '최신순' },
{ id: 'distance', label: '서울' },
{ id: 'salary', label: '전체' },
]

function getFilterItems(mode: AlbaFindMode) {
return mode === 'region' ? REGION_FILTER_ITEMS : NEARBY_FILTER_ITEMS
}

export function AlbaFindCategoryBar({
mode,
onModeChange,
activeFilter,
onFilterChange,
}: AlbaFindCategoryBarProps) {
const filterItems = getFilterItems(mode)

return (
<div className="flex flex-col gap-3">
<div className="flex h-12 gap-1 rounded-lg bg-bg-dark p-1" role="tablist">
<button
type="button"
role="tab"
aria-selected={mode === 'nearby'}
onClick={() => onModeChange('nearby')}
className={`min-h-10 flex-1 rounded-lg typography-body01-semibold transition-colors ${
mode === 'nearby'
? 'border border-line-2 bg-white text-text-100 shadow-[0px_1px_4px_0px_rgba(0,0,0,0.12)]'
: 'bg-transparent text-text-50'
}`}
>
주변에서 찾기
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'region'}
onClick={() => onModeChange('region')}
className={`min-h-10 flex-1 rounded-lg typography-body01-semibold transition-colors ${
mode === 'region'
? 'border border-line-2 bg-white text-text-100 shadow-[0px_1px_4px_0px_rgba(0,0,0,0.12)]'
: 'bg-transparent text-text-50'
}`}
>
지역에서 찾기
</button>
</div>

<div className="flex flex-wrap items-center gap-2">
{filterItems.map(({ id, label }, index) => {
const active = activeFilter === id
const showChevron = mode === 'nearby' || id === 'sort'
return (
<div key={id} className="flex items-center gap-2">
{index === 1 ? (
<div
className="h-[26px] w-px shrink-0 bg-[#d9d9d9]"
aria-hidden
/>
) : null}
<button
type="button"
onClick={() => onFilterChange(id)}
className={`inline-flex h-[26px] min-w-0 items-center gap-1 rounded-full px-3 transition-colors ${
active
? 'bg-main typography-body03-semibold text-text-100'
: 'border border-line-2 bg-white typography-body03-regular text-text-90'
}`}
>
<span>{label}</span>
{showChevron ? (
<ChevrondownIcon
className={active ? 'text-text-100' : 'text-text-70'}
/>
) : null}
</button>
</div>
)
})}
</div>
</div>
)
}
Loading
Loading