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
4 changes: 4 additions & 0 deletions apps/web/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const AdminDashboardPage = () => {
setActiveTab,
statusFilter,
setStatusFilter,
affiliationFilter,
setAffiliationFilter,
totalCount,
pagination,
pendingCounts,
Expand Down Expand Up @@ -57,6 +59,8 @@ const AdminDashboardPage = () => {
totalCount={totalCount}
pendingCounts={pendingCounts}
statusFilter={statusFilter}
affiliationFilter={affiliationFilter}
onAffiliationFilterChange={setAffiliationFilter}
onLogout={handleLogout}
>
<TabFilterBar
Expand Down
17 changes: 17 additions & 0 deletions apps/web/server/service/admin-club.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class AdminClubService {

async getAdminClubs({
status,
affiliation_type: affiliationType,
offset,
limit,
}: AdminClubsQuery): Promise<AdminPaginatedList<'clubs', AdminClubItem>> {
Expand All @@ -147,6 +148,9 @@ export class AdminClubService {
if (status) {
where.status = status
}
if (affiliationType) {
where.affiliationType = affiliationType
}

const [clubs, totalCount] = await this.clubRepository.findAndCount({
where,
Expand Down Expand Up @@ -315,6 +319,7 @@ export class AdminClubService {
async getAdminClubHistories({
club_uuid: clubUuid,
query,
affiliation_type: affiliationType,
offset,
limit,
}: AdminClubHistoriesQuery): Promise<{ total_count: number; histories: AdminClubHistoryItem[] }> {
Expand All @@ -334,6 +339,10 @@ export class AdminClubService {
baseQuery.andWhere('history.club_id = :clubUuid', { clubUuid })
}

if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}

if (trimmedQuery) {
baseQuery.andWhere(
'(club.name ILIKE :query OR manager.name ILIKE :query OR app_user.name ILIKE :query)',
Expand Down Expand Up @@ -393,6 +402,7 @@ export class AdminClubService {

async getAdminClubManagerRequests({
status,
affiliation_type: affiliationType,
offset,
limit,
}: AdminClubManagerRequestsQuery): Promise<
Expand All @@ -405,6 +415,9 @@ export class AdminClubService {
if (status) {
baseQuery.andWhere('manager_request.status = :status', { status })
}
if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}

const totalCount = await baseQuery.getCount()
const requests = await baseQuery
Expand Down Expand Up @@ -462,6 +475,7 @@ export class AdminClubService {

async getAdminClubVerificationRequests({
status,
affiliation_type: affiliationType,
offset,
limit,
}: AdminClubVerificationRequestsQuery): Promise<
Expand All @@ -474,6 +488,9 @@ export class AdminClubService {
if (status) {
baseQuery.andWhere('verification_request.status = :status', { status })
}
if (affiliationType) {
baseQuery.andWhere('club.affiliation_type = :affiliationType', { affiliationType })
}

const totalCount = await baseQuery.getCount()
const requests = await baseQuery
Expand Down
33 changes: 28 additions & 5 deletions apps/web/src/admin/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ADMIN_AUTH_TOKEN_KEY, buildQuery } from 'src/admin/constants'
import type { ClubStatus, StatusFilter } from 'src/admin/types'
import type { AffiliationFilter, ClubStatus, StatusFilter } from 'src/admin/types'
import type { ClubCollegeMajor } from 'src/entities/club'
import type {
AdminClubDetailResponse,
Expand Down Expand Up @@ -46,39 +46,62 @@ export const verifyAdminRole = async (token: string): Promise<void> => {
if (!res.ok) throw new Error('admin role required')
}

export const fetchClubs = (status: StatusFilter, pagination: PaginationParams = {}) =>
const getAffiliationType = (affiliationFilter: AffiliationFilter) =>
affiliationFilter === 'CENTRAL' ? '중앙동아리' : undefined

export const fetchClubs = (
status: StatusFilter,
affiliationFilter: AffiliationFilter,
pagination: PaginationParams = {},
) =>
request<AdminClubsResponse>(
`/api/v2/admin/clubs${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...pagination,
})}`,
)

export const fetchClubDetail = (uuid: string) =>
request<AdminClubDetailResponse>(`/api/v2/admin/clubs/${uuid}`)

export const fetchManagerRequests = (status: StatusFilter, pagination: PaginationParams = {}) =>
export const fetchManagerRequests = (
status: StatusFilter,
affiliationFilter: AffiliationFilter,
pagination: PaginationParams = {},
) =>
request<AdminClubManagerRequestsResponse>(
`/api/v2/admin/clubs/manager-requests${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...pagination,
})}`,
)

export const fetchVerificationRequests = (
status: StatusFilter,
affiliationFilter: AffiliationFilter,
pagination: PaginationParams = {},
) =>
request<AdminClubVerificationRequestsResponse>(
`/api/v2/admin/clubs/verifications${buildQuery({
status: status === 'ALL' ? undefined : status,
affiliation_type: getAffiliationType(affiliationFilter),
...pagination,
})}`,
)

export const fetchHistories = (query: string, pagination: PaginationParams = {}) =>
export const fetchHistories = (
query: string,
affiliationFilter: AffiliationFilter,
pagination: PaginationParams = {},
) =>
request<AdminClubHistoriesResponse>(
`/api/v2/admin/clubs/histories${buildQuery({ query, ...pagination })}`,
`/api/v2/admin/clubs/histories${buildQuery({
query,
affiliation_type: getAffiliationType(affiliationFilter),
...pagination,
})}`,
)

export const fetchCollegeMajors = async () => {
Expand Down
30 changes: 28 additions & 2 deletions apps/web/src/admin/components/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type React from 'react'
import { TABS } from 'src/admin/constants'
import type { AdminTab, StatusFilter } from 'src/admin/types'
import { AFFILIATION_FILTERS, TABS } from 'src/admin/constants'
import type { AdminTab, AffiliationFilter, StatusFilter } from 'src/admin/types'
import { StatusBadge, StatusFilterBar } from './ui'

type PendingCounts = {
Expand All @@ -15,6 +15,8 @@ export const AdminLayout = ({
totalCount,
pendingCounts,
statusFilter,
affiliationFilter,
onAffiliationFilterChange,
onLogout,
children,
}: {
Expand All @@ -23,6 +25,8 @@ export const AdminLayout = ({
totalCount: number
pendingCounts: PendingCounts
statusFilter: StatusFilter
affiliationFilter: AffiliationFilter
onAffiliationFilterChange: (filter: AffiliationFilter) => void
onLogout: () => void
children: React.ReactNode
}) => (
Expand Down Expand Up @@ -119,6 +123,28 @@ export const AdminLayout = ({
</div>
</header>

<div
className="mb-5 flex flex-wrap items-center gap-2"
role="group"
aria-label="동아리 소속 필터"
>
<span className="mr-1 text-sm font-semibold text-slate-600">소속</span>
{AFFILIATION_FILTERS.map((filter) => (
<button
key={filter.value}
type="button"
onClick={() => onAffiliationFilterChange(filter.value)}
className={`rounded-md border px-3 py-2 text-sm font-semibold transition ${
affiliationFilter === filter.value
? 'border-slate-950 bg-slate-950 text-white'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300'
}`}
>
{filter.label}
</button>
))}
</div>

{children}
</section>
</div>
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/admin/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AdminTab, ClubStatus, StatusFilter } from 'src/admin/types'
import type { AdminTab, AffiliationFilter, ClubStatus, StatusFilter } from 'src/admin/types'
import type { ClubCollegeMajor } from 'src/entities/club'

export const ADMIN_AUTH_TOKEN_KEY = 'allclear:admin-auth-token'
Expand All @@ -11,6 +11,11 @@ export const STATUS_FILTERS: { label: string; value: StatusFilter }[] = [
{ label: '반려', value: 'REJECTED' },
]

export const AFFILIATION_FILTERS: { label: string; value: AffiliationFilter }[] = [
{ label: '전체', value: 'ALL' },
{ label: '중앙동아리', value: 'CENTRAL' },
]

export const TABS: { label: string; value: AdminTab }[] = [
{ label: '동아리 신청', value: 'clubs' },
{ label: '관리자 매핑', value: 'managerRequests' },
Expand Down
56 changes: 41 additions & 15 deletions apps/web/src/admin/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
verifyAdminRole,
} from 'src/admin/api'
import { ADMIN_AUTH_TOKEN_KEY, ADMIN_PAGE_SIZE, formatCollegeMajorLabel } from 'src/admin/constants'
import type { AdminTab, ClubStatus, StatusFilter } from 'src/admin/types'
import type { AdminTab, AffiliationFilter, ClubStatus, StatusFilter } from 'src/admin/types'

const INITIAL_PAGE_BY_TAB: Record<AdminTab, number> = {
clubs: 1,
Expand All @@ -35,6 +35,7 @@ export const useAdminDashboard = () => {
const [authToken, setAuthToken] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<AdminTab>('clubs')
const [statusFilter, setStatusFilter] = useState<StatusFilter>('PENDING')
const [affiliationFilter, setAffiliationFilter] = useState<AffiliationFilter>('ALL')
const [pageByTab, setPageByTab] = useState<Record<AdminTab, number>>(INITIAL_PAGE_BY_TAB)
const [submittedHistoryQuery, setSubmittedHistoryQuery] = useState('')
const [toasts, setToasts] = useState<
Expand Down Expand Up @@ -70,9 +71,9 @@ export const useAdminDashboard = () => {
const historiesPage = pageByTab.histories

const clubsQuery = useQuery(
['admin-clubs', statusFilter, clubsPage, ADMIN_PAGE_SIZE],
['admin-clubs', statusFilter, affiliationFilter, clubsPage, ADMIN_PAGE_SIZE],
() =>
fetchClubs(statusFilter, {
fetchClubs(statusFilter, affiliationFilter, {
offset: getPageOffset(clubsPage),
limit: ADMIN_PAGE_SIZE,
}),
Expand All @@ -81,46 +82,64 @@ export const useAdminDashboard = () => {
},
)
const managerRequestsQuery = useQuery(
['admin-manager-requests', statusFilter, managerRequestsPage, ADMIN_PAGE_SIZE],
[
'admin-manager-requests',
statusFilter,
affiliationFilter,
managerRequestsPage,
ADMIN_PAGE_SIZE,
],
() =>
fetchManagerRequests(statusFilter, {
fetchManagerRequests(statusFilter, affiliationFilter, {
offset: getPageOffset(managerRequestsPage),
limit: ADMIN_PAGE_SIZE,
}),
{ enabled: activeTab === 'managerRequests' && !!authToken },
)
const verificationRequestsQuery = useQuery(
['admin-verification-requests', statusFilter, verificationRequestsPage, ADMIN_PAGE_SIZE],
[
'admin-verification-requests',
statusFilter,
affiliationFilter,
verificationRequestsPage,
ADMIN_PAGE_SIZE,
],
() =>
fetchVerificationRequests(statusFilter, {
fetchVerificationRequests(statusFilter, affiliationFilter, {
offset: getPageOffset(verificationRequestsPage),
limit: ADMIN_PAGE_SIZE,
}),
{ enabled: activeTab === 'verificationRequests' && !!authToken },
)

const clubsPendingQuery = useQuery(
['admin-clubs-pending-count'],
() => fetchClubs('PENDING', PENDING_COUNT_PAGINATION),
['admin-clubs-pending-count', affiliationFilter],
() => fetchClubs('PENDING', affiliationFilter, PENDING_COUNT_PAGINATION),
{
enabled: !!authToken,
refetchInterval: 30_000,
},
)
const managerRequestsPendingQuery = useQuery(
['admin-manager-requests-pending-count'],
() => fetchManagerRequests('PENDING', PENDING_COUNT_PAGINATION),
['admin-manager-requests-pending-count', affiliationFilter],
() => fetchManagerRequests('PENDING', affiliationFilter, PENDING_COUNT_PAGINATION),
{ enabled: !!authToken, refetchInterval: 30_000 },
)
const verificationsPendingQuery = useQuery(
['admin-verifications-pending-count'],
() => fetchVerificationRequests('PENDING', PENDING_COUNT_PAGINATION),
['admin-verifications-pending-count', affiliationFilter],
() => fetchVerificationRequests('PENDING', affiliationFilter, PENDING_COUNT_PAGINATION),
{ enabled: !!authToken, refetchInterval: 30_000 },
)
const historiesQuery = useQuery(
['admin-club-histories', submittedHistoryQuery, historiesPage, ADMIN_PAGE_SIZE],
[
'admin-club-histories',
submittedHistoryQuery,
affiliationFilter,
historiesPage,
ADMIN_PAGE_SIZE,
],
() =>
fetchHistories(submittedHistoryQuery, {
fetchHistories(submittedHistoryQuery, affiliationFilter, {
offset: getPageOffset(historiesPage),
limit: ADMIN_PAGE_SIZE,
}),
Expand Down Expand Up @@ -232,6 +251,11 @@ export const useAdminDashboard = () => {
}))
}

const handleAffiliationFilterChange = (filter: AffiliationFilter) => {
setAffiliationFilter(filter)
setPageByTab(INITIAL_PAGE_BY_TAB)
}

const handleHistorySearch = (query: string) => {
setSubmittedHistoryQuery(query)
setTabPage('histories', 1)
Expand Down Expand Up @@ -263,6 +287,8 @@ export const useAdminDashboard = () => {
setActiveTab: handleTabChange,
statusFilter,
setStatusFilter: handleStatusFilterChange,
affiliationFilter,
setAffiliationFilter: handleAffiliationFilterChange,
totalCount,
pagination: {
page: pageByTab[activeTab],
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
export type { ClubDecisionStatus as DecisionStatus, ClubStatus }

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

export type AdminClub = AdminClubsResponse['data']['clubs'][number]
Expand Down
Loading
Loading