get('mb_level'); if (Roles::isSuperAdminEquivalent($level)) { return null; // 전체 } return admin_effective_lg_idx(); } public function index(): string { $model = model(FeedbackModel::class); $builder = $model->orderBy('fb_idx', 'DESC'); $scope = $this->scopeLgIdx(); if ($scope !== null) { $builder->where('fb_lg_idx', $scope); } $status = (string) ($this->request->getGet('status') ?? ''); if (in_array($status, FeedbackModel::STATUSES, true)) { $builder->where('fb_status', $status); } $list = $builder->paginate(20); $pager = $model->pager; return view('admin/layout', [ 'title' => '사용자 의견', 'content' => view('admin/feedback/index', [ 'list' => $list, 'pager' => $pager, 'status' => $status, ]), ]); } /** * 전체 의견을 한 페이지에서 스크롤로 훑어볼 수 있는 화면(상세 클릭 없이 내용 전체 표시). */ public function all(): string { $model = model(FeedbackModel::class); $builder = $model->orderBy('fb_idx', 'DESC'); $scope = $this->scopeLgIdx(); if ($scope !== null) { $builder->where('fb_lg_idx', $scope); } $status = (string) ($this->request->getGet('status') ?? ''); if (in_array($status, FeedbackModel::STATUSES, true)) { $builder->where('fb_status', $status); } $list = $builder->findAll(); $ids = array_map(static fn ($r): int => (int) $r->fb_idx, $list); $filesByFb = []; if ($ids !== []) { $files = model(FeedbackFileModel::class) ->whereIn('ff_fb_idx', $ids) ->orderBy('ff_idx', 'ASC') ->findAll(); foreach ($files as $f) { $filesByFb[(int) $f->ff_fb_idx][] = $f; } } return view('admin/layout', [ 'title' => '사용자 의견 · 전체보기', 'content' => view('admin/feedback/all', [ 'list' => $list, 'filesByFb' => $filesByFb, 'status' => $status, ]), ]); } public function show(int $id): string|ResponseInterface { $model = model(FeedbackModel::class); $row = $model->find($id); $scope = $this->scopeLgIdx(); if ($row === null || ($scope !== null && (int) $row->fb_lg_idx !== $scope)) { return redirect()->to(site_url('admin/feedback'))->with('error', '의견을 찾을 수 없습니다.'); } $files = model(FeedbackFileModel::class) ->where('ff_fb_idx', $id) ->orderBy('ff_idx', 'ASC') ->findAll(); return view('admin/layout', [ 'title' => '의견 상세', 'content' => view('admin/feedback/show', ['row' => $row, 'files' => $files]), ]); } public function updateStatus(int $id): ResponseInterface { $model = model(FeedbackModel::class); $row = $model->find($id); $scope = $this->scopeLgIdx(); if ($row === null || ($scope !== null && (int) $row->fb_lg_idx !== $scope)) { return redirect()->to(site_url('admin/feedback'))->with('error', '의견을 찾을 수 없습니다.'); } $status = (string) $this->request->getPost('fb_status'); $memo = (string) $this->request->getPost('fb_admin_memo'); $update = ['fb_admin_memo' => mb_substr($memo, 0, 2000)]; if (in_array($status, FeedbackModel::STATUSES, true)) { $update['fb_status'] = $status; } $model->update($id, $update); // 전체보기 화면에서 저장한 경우 스크롤 위치(#fb-{id})를 유지한 채 그 화면으로 되돌아간다. $returnTo = (string) $this->request->getPost('return_to'); if ($returnTo === 'all') { return redirect()->to(site_url('admin/feedback/all') . '#fb-' . $id)->with('success', '처리 상태를 저장했습니다.'); } return redirect()->to(site_url('admin/feedback/' . $id))->with('success', '처리 상태를 저장했습니다.'); } /** 스크린샷 이미지 서빙 (관리자만, writable 보호 유지) */ public function image(int $id): ResponseInterface { $model = model(FeedbackModel::class); $row = $model->find($id); $scope = $this->scopeLgIdx(); if ($row === null || ($scope !== null && (int) $row->fb_lg_idx !== $scope) || empty($row->fb_screenshot)) { return $this->response->setStatusCode(404)->setBody('not found'); } $path = WRITEPATH . 'uploads/feedback/' . basename((string) $row->fb_screenshot); if (! is_file($path)) { return $this->response->setStatusCode(404)->setBody('not found'); } $mime = str_ends_with($path, '.png') ? 'image/png' : 'image/jpeg'; return $this->response ->setHeader('Content-Type', $mime) ->setHeader('Cache-Control', 'private, max-age=300') ->setBody((string) file_get_contents($path)); } /** 첨부 이미지(feedback_file) 서빙 — 관리자만, 테넌트 검증 */ public function file(int $ffIdx): ResponseInterface { $ff = model(FeedbackFileModel::class)->find($ffIdx); if ($ff === null) { return $this->response->setStatusCode(404)->setBody('not found'); } $fb = model(FeedbackModel::class)->find((int) $ff->ff_fb_idx); $scope = $this->scopeLgIdx(); if ($fb === null || ($scope !== null && (int) $fb->fb_lg_idx !== $scope)) { return $this->response->setStatusCode(404)->setBody('not found'); } $path = WRITEPATH . 'uploads/feedback/' . basename((string) $ff->ff_path); if (! is_file($path)) { return $this->response->setStatusCode(404)->setBody('not found'); } $mime = (string) ($ff->ff_mime ?: (str_ends_with($path, '.png') ? 'image/png' : 'image/jpeg')); return $this->response ->setHeader('Content-Type', $mime) ->setHeader('Cache-Control', 'private, max-age=300') ->setBody((string) file_get_contents($path)); } }