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
3 changes: 1 addition & 2 deletions apps/web/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ const AdminDashboardPage = () => {
<AdminLayout
activeTab={activeTab}
onTabChange={setActiveTab}
totalCount={totalCount}
pendingCounts={pendingCounts}
statusFilter={statusFilter}
affiliationFilter={affiliationFilter}
onAffiliationFilterChange={setAffiliationFilter}
onLogout={handleLogout}
Expand All @@ -68,6 +66,7 @@ const AdminDashboardPage = () => {
statusFilter={statusFilter}
onStatusChange={setStatusFilter}
pendingCount={pendingCounts[activeTab as keyof typeof pendingCounts] ?? 0}
totalCount={totalCount}
/>
{activeTab === 'clubs' && (
<ClubsTab
Expand Down
24 changes: 23 additions & 1 deletion apps/web/server/service/admin-club.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
AdminClubVerificationRequestStatusUpdate,
AdminClubVerificationRequestsQuery,
} from 'src/lib/schemas/admin'
import { type FindOptionsWhere, In, IsNull, type Repository } from 'typeorm'
import { type FindOptionsWhere, In, IsNull, Not, type Repository } from 'typeorm'
import {
ClubEntity,
ClubHistoryEntity,
Expand Down Expand Up @@ -139,6 +139,7 @@ export class AdminClubService {
async getAdminClubs({
status,
affiliation_type: affiliationType,
exclude_affiliation_type: excludeAffiliationType,
offset,
limit,
}: AdminClubsQuery): Promise<AdminPaginatedList<'clubs', AdminClubItem>> {
Expand All @@ -151,6 +152,9 @@ export class AdminClubService {
if (affiliationType) {
where.affiliationType = affiliationType
}
if (excludeAffiliationType) {
where.affiliationType = Not(excludeAffiliationType)
}

const [clubs, totalCount] = await this.clubRepository.findAndCount({
where,
Expand Down Expand Up @@ -320,6 +324,7 @@ export class AdminClubService {
club_uuid: clubUuid,
query,
affiliation_type: affiliationType,
exclude_affiliation_type: excludeAffiliationType,
offset,
limit,
}: AdminClubHistoriesQuery): Promise<{ total_count: number; histories: AdminClubHistoryItem[] }> {
Expand All @@ -342,6 +347,11 @@ export class AdminClubService {
if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}
if (excludeAffiliationType) {
baseQuery.andWhere('club.affiliation_type != :excludeAffiliationType', {
excludeAffiliationType,
})
}

if (trimmedQuery) {
baseQuery.andWhere(
Expand Down Expand Up @@ -403,6 +413,7 @@ export class AdminClubService {
async getAdminClubManagerRequests({
status,
affiliation_type: affiliationType,
exclude_affiliation_type: excludeAffiliationType,
offset,
limit,
}: AdminClubManagerRequestsQuery): Promise<
Expand All @@ -418,6 +429,11 @@ export class AdminClubService {
if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}
if (excludeAffiliationType) {
baseQuery.andWhere('club.affiliation_type != :excludeAffiliationType', {
excludeAffiliationType,
})
}

const totalCount = await baseQuery.getCount()
const requests = await baseQuery
Expand Down Expand Up @@ -476,6 +492,7 @@ export class AdminClubService {
async getAdminClubVerificationRequests({
status,
affiliation_type: affiliationType,
exclude_affiliation_type: excludeAffiliationType,
offset,
limit,
}: AdminClubVerificationRequestsQuery): Promise<
Expand All @@ -491,6 +508,11 @@ export class AdminClubService {
if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}
if (excludeAffiliationType) {
baseQuery.andWhere('club.affiliation_type != :excludeAffiliationType', {
excludeAffiliationType,
})
}

const totalCount = await baseQuery.getCount()
const requests = await baseQuery
Expand Down
14 changes: 8 additions & 6 deletions apps/web/src/admin/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ export const verifyAdminRole = async (token: string): Promise<void> => {
if (!res.ok) throw new Error('admin role required')
}

const getAffiliationType = (affiliationFilter: AffiliationFilter) =>
affiliationFilter === 'CENTRAL' ? '중앙동아리' : undefined
const getAffiliationFilterParams = (affiliationFilter: AffiliationFilter) => ({
affiliation_type: affiliationFilter === 'CENTRAL' ? '중앙동아리' : undefined,
exclude_affiliation_type: affiliationFilter === 'NON_CENTRAL' ? '중앙동아리' : undefined,
})

export const fetchClubs = (
status: StatusFilter,
Expand All @@ -57,7 +59,7 @@ export const fetchClubs = (
request<AdminClubsResponse>(
`/api/v2/admin/clubs${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...getAffiliationFilterParams(affiliationFilter),
...pagination,
})}`,
)
Expand All @@ -73,7 +75,7 @@ export const fetchManagerRequests = (
request<AdminClubManagerRequestsResponse>(
`/api/v2/admin/clubs/manager-requests${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...getAffiliationFilterParams(affiliationFilter),
...pagination,
})}`,
)
Expand All @@ -86,7 +88,7 @@ export const fetchVerificationRequests = (
request<AdminClubVerificationRequestsResponse>(
`/api/v2/admin/clubs/verifications${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...getAffiliationFilterParams(affiliationFilter),
...pagination,
})}`,
)
Expand All @@ -99,7 +101,7 @@ export const fetchHistories = (
request<AdminClubHistoriesResponse>(
`/api/v2/admin/clubs/histories${buildQuery({
query,
affiliation_type: getAffiliationType(affiliationFilter),
...getAffiliationFilterParams(affiliationFilter),
...pagination,
})}`,
)
Expand Down
35 changes: 25 additions & 10 deletions apps/web/src/admin/components/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ type PendingCounts = {
export const AdminLayout = ({
activeTab,
onTabChange,
totalCount,
pendingCounts,
statusFilter,
affiliationFilter,
onAffiliationFilterChange,
onLogout,
children,
}: {
activeTab: AdminTab
onTabChange: (tab: AdminTab) => void
totalCount: number
pendingCounts: PendingCounts
statusFilter: StatusFilter
affiliationFilter: AffiliationFilter
onAffiliationFilterChange: (filter: AffiliationFilter) => void
onLogout: () => void
Expand Down Expand Up @@ -109,10 +105,6 @@ export const AdminLayout = ({
</h2>
</div>
<div className="flex items-center gap-3">
<StatusBadge status={statusFilter === 'ALL' ? undefined : statusFilter} />
<span className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-semibold">
총 {totalCount.toLocaleString()}건
</span>
<button
type="button"
onClick={onLogout}
Expand Down Expand Up @@ -156,14 +148,37 @@ export const TabFilterBar = ({
statusFilter,
onStatusChange,
pendingCount,
totalCount,
}: {
activeTab: AdminTab
statusFilter: StatusFilter
onStatusChange: (status: StatusFilter) => void
pendingCount: number
totalCount: number
}) => {
if (activeTab === 'histories') return null
if (activeTab === 'histories') {
return (
<div className="mb-5 flex justify-end">
<span className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-semibold">
총 {totalCount.toLocaleString()}건
</span>
</div>
)
}

return (
<StatusFilterBar value={statusFilter} onChange={onStatusChange} pendingCount={pendingCount} />
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<StatusFilterBar
value={statusFilter}
onChange={onStatusChange}
pendingCount={pendingCount}
/>
<div className="flex items-center gap-3 self-end md:self-auto">
<StatusBadge status={statusFilter === 'ALL' ? undefined : statusFilter} />
<span className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-semibold">
총 {totalCount.toLocaleString()}건
</span>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion apps/web/src/admin/components/ClubsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const ClubList = ({
}) => {
if (isLoading) return <LoadingRows />
if (error) return <ErrorState error={error} />
if (!clubs.length) return <EmptyState title="조건에 맞는 동아리 신청이 없습니다." />
if (!clubs.length) return <EmptyState title="조건에 맞는 신규 동아리 등록 요청이 없습니다." />

return (
<div className="overflow-hidden rounded-md border border-slate-200 bg-white">
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/admin/components/ManagerRequestsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const ManagerRequestsTab = ({
}) => {
if (isLoading) return <LoadingRows />
if (error) return <ErrorState error={error} />
if (!requests.length) return <EmptyState title="조건에 맞는 관리자 매핑 신청이 없습니다." />
if (!requests.length)
return <EmptyState title="조건에 맞는 기존 동아리 운영진 등록 요청이 없습니다." />

return (
<div className="grid gap-3">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/admin/components/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const StatusFilterBar = ({
onChange: (status: StatusFilter) => void
pendingCount?: number
}) => (
<div className="mb-5 overflow-x-auto">
<div className="overflow-x-auto">
<div className="flex min-w-max gap-2 pb-1 pl-px pr-1 pt-1">
{STATUS_FILTERS.map((filter) => (
<button
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/admin/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ export const STATUS_FILTERS: { label: string; value: StatusFilter }[] = [
export const AFFILIATION_FILTERS: { label: string; value: AffiliationFilter }[] = [
{ label: '전체', value: 'ALL' },
{ label: '중앙동아리', value: 'CENTRAL' },
{ label: '중앙동아리 제외', value: 'NON_CENTRAL' },
]

export const TABS: { label: string; value: AdminTab }[] = [
{ label: '동아리 신청', value: 'clubs' },
{ label: '관리자 매핑', value: 'managerRequests' },
{ label: '공식 인증', value: 'verificationRequests' },
{ label: '신규 동아리 등록 요청', value: 'clubs' },
{ label: '기존 동아리 운영진 등록 요청', value: 'managerRequests' },
{ label: '공식 인증 요청', value: 'verificationRequests' },
{ label: '수정 이력', value: 'histories' },
]

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
export type { ClubDecisionStatus as DecisionStatus, ClubStatus }

export type StatusFilter = ClubStatus | 'ALL'
export type AffiliationFilter = 'ALL' | 'CENTRAL'
export type AffiliationFilter = 'ALL' | 'CENTRAL' | 'NON_CENTRAL'
export type AdminTab = 'clubs' | 'managerRequests' | 'verificationRequests' | 'histories'

export type AdminClub = AdminClubsResponse['data']['clubs'][number]
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/schemas/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const AdminClubsQuerySchema = z
.object({
status: z.enum(CLUB_STATUSES).optional(),
affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
exclude_affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
...AdminPaginationQueryFields,
})
.openapi('AdminClubsQuery')
Expand Down Expand Up @@ -140,6 +141,7 @@ export const AdminClubHistoriesQuerySchema = z
club_uuid: z.string().uuid().optional(),
query: z.string().trim().optional(),
affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
exclude_affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
...AdminPaginationQueryFields,
})
.openapi('AdminClubHistoriesQuery')
Expand Down Expand Up @@ -181,6 +183,7 @@ export const AdminClubManagerRequestsQuerySchema = z
.object({
status: z.enum(CLUB_STATUSES).optional(),
affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
exclude_affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
...AdminPaginationQueryFields,
})
.openapi('AdminClubManagerRequestsQuery')
Expand Down Expand Up @@ -224,6 +227,7 @@ export const AdminClubVerificationRequestsQuerySchema = z
.object({
status: z.enum(CLUB_STATUSES).optional(),
affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
exclude_affiliation_type: z.enum(ADMIN_CLUB_AFFILIATION_TYPES).optional(),
...AdminPaginationQueryFields,
})
.openapi('AdminClubVerificationRequestsQuery')
Expand Down
Loading