Function bodies 156 total
BulkCancelPage function · typescript · L4-L10 (7 LOC)apps/admin/src/app/bulk-cancel/page.tsx
export default function BulkCancelPage() {
return (
<Suspense fallback={null}>
<BulkCancelClient />
</Suspense>
);
}toCertArray function · typescript · L30-L40 (11 LOC)apps/admin/src/app/instructors/[id]/page.tsx
function toCertArray(input: unknown): Certification[] {
if (!Array.isArray(input)) return [];
return input
.filter((x) => x && typeof x === 'object')
.map((x: any) => ({
type: String(x.type ?? ''),
label: String(x.label ?? ''),
iconType: x.iconType ? String(x.iconType) : undefined,
}))
.filter((x) => x.type && x.label);
}certsCount function · typescript · L43-L51 (9 LOC)apps/admin/src/app/instructors/page.tsx
function certsCount(certs: unknown): number {
if (Array.isArray(certs)) return certs.length;
if (certs && typeof certs === "object") {
// sometimes stored as JSON object; count values conservatively
const v = Object.values(certs as Record<string, unknown>);
return Array.isArray(v) ? v.length : 0;
}
return 0;
}InstructorsPage function · typescript · L53-L210 (158 LOC)apps/admin/src/app/instructors/page.tsx
export default function InstructorsPage() {
const [tab, setTab] = useState<"ALL" | InstructorStatus>("APPLIED");
const [search, setSearch] = useState<string>("");
const [page, setPage] = useState<number>(1);
const [data, setData] = useState<Paginated<Instructor> | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<{ message: string; requestId: string | null } | null>(null);
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const queryString = useMemo(() => {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", "20");
if (tab !== "ALL") params.set("instructorStatus", tab);
if (search.trim().length > 0) params.set("search", search.trim());
return params.toString();
}, [page, tab, search]);
const fetchList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await api.get<Paginated<Instructor>>(`/adRootLayout function · typescript · L10-L18 (9 LOC)apps/admin/src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}LoginPage function · typescript · L16-L100 (85 LOC)apps/admin/src/app/login/page.tsx
export default function LoginPage() {
const { user, isLoading, login } = useAuth();
const router = useRouter();
const [error, setError] = useState('');
const [logging, setLogging] = useState(false);
useEffect(() => {
if (!isLoading && user?.role === 'ADMIN') {
router.replace('/');
}
}, [user, isLoading, router]);
useEffect(() => {
if (!KAKAO_JS_KEY) return;
const script = document.createElement('script');
script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js';
script.onload = () => {
if (window.Kakao && !window.Kakao.isInitialized()) {
window.Kakao.init(KAKAO_JS_KEY);
}
};
document.head.appendChild(script);
}, []);
const handleKakaoLogin = () => {
if (!window.Kakao) {
setError('Kakao SDK가 로드되지 않았습니다');
return;
}
window.Kakao.Auth.login({
success: async (authObj: { access_token: string }) => {
setLogging(true);
setError('');
try {
DashboardPage function · typescript · L84-L198 (115 LOC)apps/admin/src/app/page.tsx
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<ErrorState | null>(null);
const fetchStats = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await api.get<DashboardStats>('/admin/dashboard/stats');
setStats(data);
} catch (e) {
if (e instanceof ApiError) {
setError({ message: e.message, code: e.code, requestId: e.requestId });
} else {
setError({ message: '대시보드 통계를 불러오지 못했습니다', code: null, requestId: null });
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStats();
}, [fetchStats]);
// M2: Compute max value for proportional bar widths (no derived metrics — just layout scaling)
const maxValue = stats
? Math.max(...CARD_CONFIGS.map((c) => stats[c.key]), 1)
: 1;
return (
<AdminLayout>
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
ProgramsPendingPage function · typescript · L23-L108 (86 LOC)apps/admin/src/app/programs/pending/page.tsx
export default function ProgramsPendingPage() {
const [data, setData] = useState<ProgramsResponse | null>(null);
const [page, setPage] = useState(1);
const load = useCallback(() => {
api
.get<ProgramsResponse>(
`/admin/programs?approvalStatus=PENDING_REVIEW&page=${page}&limit=20`,
)
.then(setData);
}, [page]);
useEffect(() => {
load();
}, [load]);
const handleApprove = async (id: string) => {
if (!confirm('승인하시겠습니까?')) return;
await api.patch(`/admin/programs/${id}/approve`);
load();
};
const handleReject = async (id: string) => {
const reason = prompt('거절 사유를 입력하세요');
if (!reason) return;
await api.patch(`/admin/programs/${id}/reject`, { rejectionReason: reason });
load();
};
return (
<AdminLayout>
<h2 className="text-xl font-bold mb-6">승인 대기 프로그램</h2>
{!data ? (
<p className="text-gray-500">로딩 중...</p>
) : data.items.length === 0 ? (
<p className="text-gray-5ReviewsPage function · typescript · L26-L153 (128 LOC)apps/admin/src/app/reviews/page.tsx
export default function ReviewsPage() {
const [data, setData] = useState<ReviewsResponse | null>(null);
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState('');
const [ratingFilter, setRatingFilter] = useState('');
const load = useCallback(() => {
const params = new URLSearchParams({ page: String(page), limit: '20' });
if (statusFilter) params.set('status', statusFilter);
if (ratingFilter) params.set('rating', ratingFilter);
api.get<ReviewsResponse>(`/admin/reviews?${params}`).then(setData);
}, [page, statusFilter, ratingFilter]);
useEffect(() => {
load();
}, [load]);
const handleSetStatus = async (id: string, newStatus: 'VISIBLE' | 'HIDDEN') => {
await api.patch(`/admin/reviews/${id}/status`, { status: newStatus });
load();
};
const renderStars = (rating: number) => {
return '★'.repeat(rating) + '☆'.repeat(5 - rating);
};
return (
<AdminLayout>
<h2 className="text-xl font-bold mbSettlementsPage function · typescript · L34-L136 (103 LOC)apps/admin/src/app/settlements/page.tsx
export default function SettlementsPage() {
const [data, setData] = useState<SettlementsResponse | null>(null);
const [page, setPage] = useState(1);
const [status, setStatus] = useState('');
const load = useCallback(() => {
const params = new URLSearchParams({ page: String(page), limit: '20' });
if (status) params.set('status', status);
api.get<SettlementsResponse>(`/admin/settlements?${params}`).then(setData);
}, [page, status]);
useEffect(() => {
load();
}, [load]);
const handlePay = async (id: string) => {
if (!confirm('지급 처리하시겠습니까?')) return;
await api.patch(`/admin/settlements/${id}/pay`);
load();
};
return (
<AdminLayout>
<h2 className="text-xl font-bold mb-6">정산 관리</h2>
<div className="mb-4 flex gap-2">
{STATUS_OPTIONS.map((s) => (
<button
key={s}
onClick={() => {
setStatus(s);
setPage(1);
}}
className={`px-3 py-1.5 texUsersPage function · typescript · L32-L140 (109 LOC)apps/admin/src/app/users/page.tsx
export default function UsersPage() {
const [data, setData] = useState<UsersResponse | null>(null);
const [page, setPage] = useState(1);
const [role, setRole] = useState('');
const [search, setSearch] = useState('');
const load = useCallback(() => {
const params = new URLSearchParams({ page: String(page), limit: '20' });
if (role) params.set('role', role);
if (search) params.set('search', search);
api.get<UsersResponse>(`/admin/users?${params}`).then(setData);
}, [page, role, search]);
useEffect(() => {
load();
}, [load]);
const handleRoleChange = async (userId: string, newRole: string) => {
if (!confirm(`역할을 ${ROLE_LABELS[newRole] || newRole}(으)로 변경하시겠습니까?`)) return;
await api.patch(`/admin/users/${userId}/role`, { role: newRole });
load();
};
return (
<AdminLayout>
<h2 className="text-xl font-bold mb-6">유저 관리</h2>
<div className="mb-4 flex gap-4 items-center">
<div className="flex gap-2">
{ROLAdminLayout function · typescript · L7-L16 (10 LOC)apps/admin/src/components/AdminLayout.tsx
export default function AdminLayout({ children }: { children: ReactNode }) {
return (
<ProtectedRoute>
<div className="flex min-h-screen">
<Sidebar />
<main className="flex-1 bg-gray-50 p-8">{children}</main>
</div>
</ProtectedRoute>
);
}Pagination function · typescript · L10-L53 (44 LOC)apps/admin/src/components/Pagination.tsx
export default function Pagination({ page, total, pageSize, onChange }: PaginationProps) {
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) return null;
return (
<div className="flex items-center justify-between mt-4">
<p className="text-sm text-gray-600">
총 {total}건 (페이지 {page}/{totalPages})
</p>
<div className="flex gap-1">
<button
disabled={page <= 1}
onClick={() => onChange(page - 1)}
className="px-3 py-1 text-sm border rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100"
>
이전
</button>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
const start = Math.max(1, Math.min(page - 2, totalPages - 4));
const p = start + i;
if (p > totalPages) return null;
return (
<button
key={p}
onClick={() => onChange(p)}
className={`px-3 py-1 text-sProtectedRoute function · typescript · L7-L30 (24 LOC)apps/admin/src/components/ProtectedRoute.tsx
export default function ProtectedRoute({ children }: { children: ReactNode }) {
const { user, isLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && (!user || user.role !== 'ADMIN')) {
router.replace('/login');
}
}, [user, isLoading, router]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<p className="text-gray-500">로딩 중...</p>
</div>
);
}
if (!user || user.role !== 'ADMIN') {
return null;
}
return <>{children}</>;
}Sidebar function · typescript · L18-L58 (41 LOC)apps/admin/src/components/Sidebar.tsx
export default function Sidebar() {
const pathname = usePathname();
const { logout } = useAuth();
return (
<aside className="w-60 bg-gray-900 text-gray-100 min-h-screen flex flex-col">
<div className="px-6 py-5 border-b border-gray-700">
<h1 className="text-lg font-bold tracking-tight">숲똑 Admin</h1>
</div>
<nav className="flex-1 py-4">
{NAV_ITEMS.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`block px-6 py-2.5 text-sm transition-colors ${
isActive
? 'bg-gray-800 text-white font-medium'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
}`}
>
{item.label}
</Link>
);
})}
</nav>
<div Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
AuthProvider function · typescript · L28-L73 (46 LOC)apps/admin/src/contexts/AuthContext.tsx
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const savedToken = localStorage.getItem('token');
const savedUser = localStorage.getItem('user');
if (savedToken && savedUser) {
try {
const parsed = JSON.parse(savedUser);
if (parsed.role === 'ADMIN') {
setToken(savedToken);
setUser(parsed);
} else {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
} catch {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
}
setIsLoading(false);
}, []);
const login = useCallback((newToken: string, newUser: User) => {
setToken(newToken);
setUser(newUser);
localStorage.setItem('token', newToken);
localStorage.setItem(request function · typescript · L3-L34 (32 LOC)apps/admin/src/services/api.ts
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...((options.headers as Record<string, string>) || {}),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers,
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
const err = body?.error ?? body;
throw new ApiError(
res.status,
err?.message || res.statusText,
err?.code ?? null,
err?.requestId ?? null,
);
}
if (res.status === 204) return null as T;
return res.json();
}ApiError.constructor method · typescript · L37-L44 (8 LOC)apps/admin/src/services/api.ts
constructor(
public status: number,
message: string,
public code: string | null = null,
public requestId: string | null = null,
) {
super(message);
}App function · typescript · L4-L12 (9 LOC)apps/mobile/App.tsx
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.title}>숲똑</Text>
<Text style={styles.subtitle}>숲체험 예약 플랫폼</Text>
<StatusBar style="auto" />
</View>
);
}AdminBulkCancelService.determineMode method · typescript · L82-L87 (6 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
private determineMode(): BulkCancelMode {
const paymentsServicePresent =
!!this.paymentsService &&
typeof this.paymentsService.processRefund === 'function';
return getRefundMode(paymentsServicePresent);
}AdminBulkCancelService.createJob method · typescript · L107-L186 (80 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
async createJob(
sessionId: string,
reason: string,
adminUserId: string,
dryRun = false,
): Promise<CreateJobDryRunResult | CreateJobCreatedResult> {
const program = await this.prisma.program.findUnique({
where: { id: sessionId },
});
if (!program) {
throw new BusinessException(
'BULK_CANCEL_JOB_NOT_FOUND',
'프로그램을 찾을 수 없습니다',
404,
);
}
const runningJob = await this.prisma.bulkCancelJob.findFirst({
where: { sessionId, status: 'RUNNING' },
});
if (runningJob) {
throw new BusinessException(
'BULK_CANCEL_JOB_RUNNING',
'해당 프로그램에 이미 실행 중인 일괄 취소 작업이 있습니다',
409,
);
}
const targetReservations = await this.prisma.reservation.findMany({
where: {
programId: sessionId,
status: { in: ['PENDING', 'CONFIRMED'] },
},
include: { payment: true, program: true },
});
const mode = this.determineMode();
if (dryRun) {
AdminBulkCancelService.startJob method · typescript · L188-L256 (69 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
async startJob(jobId: string): Promise<StartJobResult> {
const job = await this.prisma.bulkCancelJob.findUnique({
where: { id: jobId },
include: {
items: {
include: {
reservation: { include: { payment: true, program: true } },
},
},
},
});
if (!job) {
throw new BusinessException(
'BULK_CANCEL_JOB_NOT_FOUND',
'일괄 취소 작업을 찾을 수 없습니다',
404,
);
}
if (
job.status === 'COMPLETED' ||
job.status === 'COMPLETED_WITH_ERRORS' ||
job.status === 'FAILED'
) {
throw new BusinessException(
'BULK_CANCEL_JOB_COMPLETED',
'이미 완료된 작업입니다',
409,
);
}
if (job.status === 'RUNNING') {
return { message: '이미 실행 중입니다', jobId: job.id };
}
await this.prisma.bulkCancelJob.update({
where: { id: jobId },
data: { status: 'RUNNING', startedAt: new Date() },
});
let successCount = 0;
let failedCAdminBulkCancelService.processItem method · typescript · L258-L355 (98 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
private async processItem(
item: ProcessItemInput,
mode: string,
): Promise<BulkCancelItemResult> {
if (item.result === 'SUCCESS') {
return 'SKIPPED';
}
const reservation = item.reservation;
if (shouldSkipBulkCancel(reservation)) {
await this.prisma.bulkCancelJobItem.update({
where: { id: item.id },
data: { result: 'SKIPPED', attemptedAt: new Date() },
});
return 'SKIPPED';
}
const refundAmount = this.calculateRefundAmount(
reservation.totalPrice,
reservation.program.scheduleAt,
);
try {
if (
mode === 'A_PG_REFUND' &&
reservation.payment &&
refundAmount > 0 &&
this.paymentsService
) {
await this.paymentsService.processRefund(reservation.id, refundAmount);
}
await this.prisma.$transaction(async (tx) => {
await tx.reservation.update({
where: { id: reservation.id },
data: { status: 'CANCELLED' },
Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
AdminBulkCancelService.calculateRefundAmount method · typescript · L357-L375 (19 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
private calculateRefundAmount(
totalPrice: number,
scheduleAt: Date,
): number {
const now = new Date();
const diffMs = scheduleAt.getTime() - now.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
let refundRatio: number;
if (diffDays >= 2) {
refundRatio = REFUND_POLICY.DAYS_BEFORE_2;
} else if (diffDays >= 1) {
refundRatio = REFUND_POLICY.DAYS_BEFORE_1;
} else {
refundRatio = REFUND_POLICY.SAME_DAY;
}
return Math.floor(totalPrice * refundRatio);
}AdminBulkCancelService.deriveFinalStatus method · typescript · L377-L385 (9 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
private deriveFinalStatus(
successCount: number,
failedCount: number,
skippedCount: number,
): BulkCancelJobStatus {
if (failedCount === 0) return 'COMPLETED';
if (successCount > 0 || skippedCount > 0) return 'COMPLETED_WITH_ERRORS';
return 'FAILED';
}AdminBulkCancelService.getJobSummary method · typescript · L387-L404 (18 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
async getJobSummary(jobId: string) {
const job = await this.prisma.bulkCancelJob.findUnique({
where: { id: jobId },
include: {
program: { select: { id: true, title: true, scheduleAt: true } },
},
});
if (!job) {
throw new BusinessException(
'BULK_CANCEL_JOB_NOT_FOUND',
'일괄 취소 작업을 찾을 수 없습니다',
404,
);
}
return job;
}AdminBulkCancelService.getJobItems method · typescript · L406-L455 (50 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
async getJobItems(
jobId: string,
page = 1,
pageSize = 20,
result?: BulkCancelItemResult,
) {
const job = await this.prisma.bulkCancelJob.findUnique({
where: { id: jobId },
});
if (!job) {
throw new BusinessException(
'BULK_CANCEL_JOB_NOT_FOUND',
'일괄 취소 작업을 찾을 수 없습니다',
404,
);
}
const where: { jobId: string; result?: BulkCancelItemResult } = { jobId };
if (result) {
where.result = result;
}
const [items, total] = await Promise.all([
this.prisma.bulkCancelJobItem.findMany({
where,
include: {
reservation: {
select: {
id: true,
userId: true,
totalPrice: true,
status: true,
},
},
},
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { attemptedAt: 'desc' },
}),
this.prisma.bulkCancelJobItem.count({ where }),
]);
reAdminBulkCancelService.retryFailed method · typescript · L457-L513 (57 LOC)apps/server/src/admin/admin-bulk-cancel.service.ts
async retryFailed(jobId: string): Promise<RetryFailedResult> {
const job = await this.prisma.bulkCancelJob.findUnique({
where: { id: jobId },
include: {
items: {
where: { result: 'FAILED' },
include: {
reservation: { include: { payment: true, program: true } },
},
},
},
});
if (!job) {
throw new BusinessException(
'BULK_CANCEL_JOB_NOT_FOUND',
'일괄 취소 작업을 찾을 수 없습니다',
404,
);
}
if (job.items.length === 0) {
return { message: '재시도할 실패 항목이 없습니다', jobId: job.id };
}
await this.prisma.bulkCancelJob.update({
where: { id: jobId },
data: { status: 'RUNNING', startedAt: new Date(), finishedAt: null },
});
let successCount = job.successCount;
let failedCount = 0;
const skippedCount = job.skippedCount;
for (const item of job.items) {
const result = await this.processItem(item, job.mode);
if (result === 'SAdminController.constructor method · typescript · L53-L58 (6 LOC)apps/server/src/admin/admin.controller.ts
constructor(
private adminService: AdminService,
private settlementsService: SettlementsService,
private bulkCancelService: AdminBulkCancelService,
private categoriesService: CategoriesService,
) {}AdminController.getBulkCancelJobItems method · typescript · L287-L297 (11 LOC)apps/server/src/admin/admin.controller.ts
getBulkCancelJobItems(
@Param('jobId') jobId: string,
@Query() query: QueryBulkCancelItemsDto,
) {
return this.bulkCancelService.getJobItems(
jobId,
query.page,
query.pageSize,
query.result,
);
}AdminService.getDashboardStats method · typescript · L37-L63 (27 LOC)apps/server/src/admin/admin.service.ts
async getDashboardStats() {
const [totalUsers, totalReservations, totalRevenue, pendingPrograms, pendingInstructors] =
await Promise.all([
this.prisma.user.count(),
this.prisma.reservation.count({
where: { status: { in: ['CONFIRMED', 'COMPLETED'] } },
}),
this.prisma.payment.aggregate({
where: { status: 'PAID' },
_sum: { amount: true },
}),
this.prisma.program.count({
where: { approvalStatus: 'PENDING_REVIEW' },
}),
this.prisma.user.count({
where: { instructorStatus: 'APPLIED' },
}),
]);
return {
totalUsers,
totalReservations,
totalRevenue: totalRevenue._sum.amount ?? 0,
pendingPrograms,
pendingInstructors,
};
}All rows above produced by Repobility · https://repobility.com
AdminService.findPrograms method · typescript · L65-L96 (32 LOC)apps/server/src/admin/admin.service.ts
async findPrograms(query: AdminQueryProgramsDto) {
const where: Prisma.ProgramWhereInput = {};
if (query.approvalStatus) {
where.approvalStatus = query.approvalStatus;
}
if (query.search) {
where.OR = [
{ title: { contains: query.search, mode: 'insensitive' } },
{ instructor: { name: { contains: query.search, mode: 'insensitive' } } },
];
}
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const [items, total] = await Promise.all([
this.prisma.program.findMany({
where,
include: {
instructor: { select: { id: true, name: true, email: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
this.prisma.program.count({ where }),
]);
return { items, total, page, limit };
}AdminService.approveProgram method · typescript · L98-L126 (29 LOC)apps/server/src/admin/admin.service.ts
async approveProgram(id: string) {
const program = await this.prisma.program.findUnique({
where: { id },
select: { id: true, title: true, instructorId: true, approvalStatus: true },
});
if (!program) {
throw new NotFoundException('프로그램을 찾을 수 없습니다');
}
if (program.approvalStatus !== 'PENDING_REVIEW') {
throw new BadRequestException('검수 대기 상태의 프로그램만 승인할 수 있습니다');
}
const updated = await this.prisma.program.update({
where: { id },
data: { approvalStatus: 'APPROVED', rejectionReason: null },
});
await this.notificationsService.createAndSend(
program.instructorId,
'PROGRAM_APPROVED',
'프로그램 승인 알림',
`"${program.title}" 프로그램이 승인되었습니다. 이제 부모님들에게 노출됩니다.`,
{ programId: program.id },
);
return updated;
}AdminService.rejectProgram method · typescript · L128-L159 (32 LOC)apps/server/src/admin/admin.service.ts
async rejectProgram(id: string, dto: RejectProgramDto) {
const program = await this.prisma.program.findUnique({
where: { id },
select: { id: true, title: true, instructorId: true, approvalStatus: true },
});
if (!program) {
throw new NotFoundException('프로그램을 찾을 수 없습니다');
}
if (program.approvalStatus !== 'PENDING_REVIEW') {
throw new BadRequestException('검수 대기 상태의 프로그램만 거절할 수 있습니다');
}
const updated = await this.prisma.program.update({
where: { id },
data: {
approvalStatus: 'REJECTED',
rejectionReason: dto.rejectionReason,
},
});
await this.notificationsService.createAndSend(
program.instructorId,
'PROGRAM_REJECTED',
'프로그램 거절 알림',
`"${program.title}" 프로그램이 거절되었습니다. 사유: ${dto.rejectionReason}`,
{ programId: program.id },
);
return updated;
}AdminService.findUsers method · typescript · L161-L198 (38 LOC)apps/server/src/admin/admin.service.ts
async findUsers(query: AdminQueryUsersDto) {
const where: Prisma.UserWhereInput = {};
if (query.role) {
where.role = query.role;
}
if (query.search) {
where.OR = [
{ name: { contains: query.search, mode: 'insensitive' } },
{ email: { contains: query.search, mode: 'insensitive' } },
];
}
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const [items, total] = await Promise.all([
this.prisma.user.findMany({
where,
select: {
id: true,
email: true,
name: true,
role: true,
phoneNumber: true,
profileImageUrl: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
this.prisma.user.count({ where }),
]);
return { items, total, page, limit };
}AdminService.changeUserRole method · typescript · L200-L217 (18 LOC)apps/server/src/admin/admin.service.ts
async changeUserRole(id: string, dto: ChangeRoleDto) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
return this.prisma.user.update({
where: { id },
data: { role: dto.role },
select: {
id: true,
email: true,
name: true,
role: true,
},
});
}AdminService.findInstructorApplications method · typescript · L219-L261 (43 LOC)apps/server/src/admin/admin.service.ts
async findInstructorApplications(query: AdminQueryInstructorsDto) {
const where: Prisma.UserWhereInput = {
role: 'INSTRUCTOR',
};
if (query.instructorStatus) {
where.instructorStatus = query.instructorStatus;
}
if (query.search) {
where.OR = [
{ name: { contains: query.search, mode: 'insensitive' } },
{ email: { contains: query.search, mode: 'insensitive' } },
];
}
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const [items, total] = await Promise.all([
this.prisma.user.findMany({
where,
select: {
id: true,
email: true,
name: true,
role: true,
phoneNumber: true,
profileImageUrl: true,
instructorStatus: true,
instructorStatusReason: true,
certifications: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
taAdminService.findInstructorById method · typescript · L263-L285 (23 LOC)apps/server/src/admin/admin.service.ts
async findInstructorById(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
role: true,
phoneNumber: true,
profileImageUrl: true,
instructorStatus: true,
instructorStatusReason: true,
certifications: true,
createdAt: true,
},
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
return user;
}AdminService.approveInstructor method · typescript · L287-L317 (31 LOC)apps/server/src/admin/admin.service.ts
async approveInstructor(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, role: true, instructorStatus: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
if (user.instructorStatus !== 'APPLIED') {
throw new BadRequestException('신청 대기 상태의 강사만 승인할 수 있습니다');
}
const updated = await this.prisma.user.update({
where: { id: userId },
data: {
instructorStatus: 'APPROVED',
instructorStatusReason: null,
},
});
await this.notificationsService.createAndSend(
userId,
'INSTRUCTOR_APPROVED',
'강사 승인 알림',
`${user.name}님의 강사 신청이 승인되었습니다. 이제 프로그램을 등록할 수 있습니다.`,
);
return updated;
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
AdminService.rejectInstructor method · typescript · L319-L349 (31 LOC)apps/server/src/admin/admin.service.ts
async rejectInstructor(userId: string, dto: RejectInstructorDto) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, role: true, instructorStatus: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
if (user.instructorStatus !== 'APPLIED') {
throw new BadRequestException('신청 대기 상태의 강사만 거절할 수 있습니다');
}
const updated = await this.prisma.user.update({
where: { id: userId },
data: {
instructorStatus: 'REJECTED',
instructorStatusReason: dto.reason,
},
});
await this.notificationsService.createAndSend(
userId,
'INSTRUCTOR_REJECTED',
'강사 신청 거절 알림',
`${user.name}님의 강사 신청이 거절되었습니다. 사유: ${dto.reason}`,
);
return updated;
}AdminService.updateInstructorCertifications method · typescript · L351-L374 (24 LOC)apps/server/src/admin/admin.service.ts
async updateInstructorCertifications(userId: string, dto: UpdateCertificationsDto) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, role: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
if (user.role !== 'INSTRUCTOR') {
throw new BadRequestException('강사만 인증 뱃지를 설정할 수 있습니다');
}
return this.prisma.user.update({
where: { id: userId },
data: { certifications: dto.certifications as any },
select: {
id: true,
name: true,
certifications: true,
},
});
}AdminService.chargeCash method · typescript · L376-L397 (22 LOC)apps/server/src/admin/admin.service.ts
async chargeCash(userId: string, dto: ChargeCashDto) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}
const updated = await this.prisma.user.update({
where: { id: userId },
data: {
messageCashBalance: { increment: dto.amount },
},
select: {
id: true,
email: true,
name: true,
messageCashBalance: true,
},
});
return updated;
}AdminService.createProvider method · typescript · L401-L408 (8 LOC)apps/server/src/admin/admin.service.ts
async createProvider(dto: CreateProviderDto) {
return this.prisma.provider.create({
data: {
name: dto.name,
regionTags: dto.regionTags ?? [],
},
});
}AdminService.findProviders method · typescript · L410-L434 (25 LOC)apps/server/src/admin/admin.service.ts
async findProviders(query: AdminQueryProvidersDto) {
const where: Prisma.ProviderWhereInput = {};
if (query.query) {
where.name = { contains: query.query, mode: 'insensitive' };
}
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const [items, total] = await Promise.all([
this.prisma.provider.findMany({
where,
include: {
profile: { select: { isPublished: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.provider.count({ where }),
]);
return { items, total, page, pageSize };
}AdminService.updateProvider method · typescript · L436-L447 (12 LOC)apps/server/src/admin/admin.service.ts
async updateProvider(id: string, dto: UpdateProviderDto) {
const provider = await this.prisma.provider.findUnique({ where: { id } });
if (!provider) {
throw new NotFoundException('업체를 찾을 수 없습니다');
}
const data: Prisma.ProviderUpdateInput = {};
if (dto.name !== undefined) data.name = dto.name;
if (dto.regionTags !== undefined) data.regionTags = dto.regionTags;
return this.prisma.provider.update({ where: { id }, data });
}AdminService.getProviderProfile method · typescript · L449-L495 (47 LOC)apps/server/src/admin/admin.service.ts
async getProviderProfile(providerId: string) {
const provider = await this.prisma.provider.findUnique({
where: { id: providerId },
});
if (!provider) {
throw new NotFoundException('업체를 찾을 수 없습니다');
}
const profile = await this.prisma.providerProfile.findUnique({
where: { providerId },
});
if (!profile) {
return {
provider: { id: provider.id, name: provider.name, regionTags: provider.regionTags },
profile: {
displayName: provider.name,
introShort: null,
certificationsText: null,
storyText: null,
coverImageUrls: [],
contactLinks: [],
isPublished: false,
},
};
}
const coverImageKeys = profile.coverImageUrls as string[];
const coverImageUrls = await Promise.all(
coverImageKeys.map((key) =>
this.storageService.generateDownloadUrl(key, GALLERY_SIGNED_URL_EXPIRES_IN),
),
);
return {
provider:AdminService.upsertProviderProfile method · typescript · L497-L519 (23 LOC)apps/server/src/admin/admin.service.ts
async upsertProviderProfile(providerId: string, dto: AdminUpsertProfileDto) {
const provider = await this.prisma.provider.findUnique({
where: { id: providerId },
});
if (!provider) {
throw new NotFoundException('업체를 찾을 수 없습니다');
}
const data = {
displayName: dto.displayName,
introShort: dto.introShort ?? null,
certificationsText: dto.certificationsText ?? null,
storyText: dto.storyText ?? null,
coverImageUrls: dto.coverImageUrls ?? [],
contactLinks: dto.contactLinks ?? [],
};
return this.prisma.providerProfile.upsert({
where: { providerId },
create: { providerId, ...data },
update: data,
});
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
AdminService.presignProviderCoverImages method · typescript · L521-L548 (28 LOC)apps/server/src/admin/admin.service.ts
async presignProviderCoverImages(providerId: string, dto: PresignCoverDto) {
const provider = await this.prisma.provider.findUnique({
where: { id: providerId },
});
if (!provider) {
throw new NotFoundException('업체를 찾을 수 없습니다');
}
const uploads = await Promise.all(
dto.files.map(async (file) => {
const ext = path.extname(file.filename);
const key = `provider-covers/${providerId}/${randomUUID()}${ext}`;
const uploadUrl = await this.storageService.generateUploadUrl(
key,
file.contentType,
PROVIDER_COVER_UPLOAD_URL_EXPIRES_IN,
);
return {
uploadUrl,
method: 'PUT',
headers: { 'Content-Type': file.contentType },
finalUrl: key,
};
}),
);
return { uploads };
}AdminService.publishProviderProfile method · typescript · L550-L562 (13 LOC)apps/server/src/admin/admin.service.ts
async publishProviderProfile(providerId: string, dto: PublishProfileDto) {
const profile = await this.prisma.providerProfile.findUnique({
where: { providerId },
});
if (!profile) {
throw new NotFoundException('프로필을 먼저 생성해주세요');
}
return this.prisma.providerProfile.update({
where: { providerId },
data: { isPublished: dto.isPublished },
});
}AdminService.findReviews method · typescript · L566-L595 (30 LOC)apps/server/src/admin/admin.service.ts
async findReviews(query: AdminQueryReviewsDto) {
const where: Prisma.ReviewWhereInput = {};
if (query.status) {
where.status = query.status;
}
if (query.rating) {
where.rating = query.rating;
}
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const [items, total] = await Promise.all([
this.prisma.review.findMany({
where,
include: {
program: { select: { id: true, title: true } },
parentUser: { select: { id: true, name: true, email: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
this.prisma.review.count({ where }),
]);
return { items, total, page, limit };
}page 1 / 4next ›