sir.kr 커뮤니티 제보(Xbuilder 님 외)와 전수조사 발굴분을 코어·게시판· 이커머스·페이지·ckeditor5 전반에 반영. - 통합 검색 하이라이트: 코어 SearchHighlighter 로 원문 이스케이프 후 강조 (저장형 XSS 차단, board/ecommerce/page 3중복제 통합) - 공개 최신글·인기글·관리자 대시보드: 읽기권한 없는 게시판·블라인드·비활성 게시판 글 필터. 비밀글은 기존 공개 정책대로 제목 노출 유지(본문만 보호) - 게시판 관리 대시보드 권한 게이트, 이커머스 쿠폰 다운로드·리뷰 대상·주문옵션 확정 스코프 강화, 취소/환불 항목 검증 회원/비회원 대칭 - 만료 토큰 거부(SetLocale), 회원 탈퇴·정보관리 실패 시 내부 오류 원문 마스킹 - 예외→응답 매핑 정비(generic catch 상태코드), ckeditor 업로드 권한 게이트 정리 정책 정합 정정: 착수 계획이 "비밀글 제목 노출=취약점"으로 전제해 공개 최신글· 대시보드에 넣었던 is_secret 필터는, 저장소가 확정한 "제목 공개, 본문만 보호" 정책(2026-01-02/02-04/06-18/06-19)과 배치되어 되돌림. 읽기권한·블라인드 필터는 유지. Chrome MCP 실측(T1~T10) + 회귀 테스트로 확인.
133 lines
4.2 KiB
PHP
133 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\User;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class SetLocale
|
|
{
|
|
/**
|
|
* 들어오는 요청을 처리하고 사용자 언어를 설정합니다.
|
|
*
|
|
* @param Request $request HTTP 요청
|
|
* @param Closure $next 다음 미들웨어 또는 요청 핸들러
|
|
* @return Response HTTP 응답
|
|
*/
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$locale = $this->determineLocale($request);
|
|
App::setLocale($locale);
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* 사용자의 언어 설정을 결정합니다.
|
|
*
|
|
* 우선순위:
|
|
* 1. 로그인한 사용자의 언어 설정 (users.language)
|
|
* 2. 브라우저 Accept-Language 헤더
|
|
* 3. 시스템 기본값 (config('app.locale'))
|
|
*
|
|
* 참고: 프론트엔드(localStorage g7_locale)에서 언어를 관리하므로
|
|
* 서버는 사용자 DB 설정만 우선 적용합니다.
|
|
*
|
|
* @param Request $request HTTP 요청
|
|
* @return string 결정된 언어 코드
|
|
*/
|
|
private function determineLocale(Request $request): string
|
|
{
|
|
$supportedLocales = config('app.supported_locales', ['ko', 'en']);
|
|
|
|
// 1. 인증된 사용자의 언어 설정 확인 (최우선)
|
|
$user = $this->resolveUser($request);
|
|
if ($user && $user->language) {
|
|
$userLocale = $user->language;
|
|
if (in_array($userLocale, $supportedLocales)) {
|
|
return $userLocale;
|
|
}
|
|
}
|
|
|
|
// 2. Accept-Language 헤더 확인 (브라우저 기본 언어)
|
|
$acceptLanguage = $request->header('Accept-Language');
|
|
if ($acceptLanguage) {
|
|
$locale = $this->parseAcceptLanguage($acceptLanguage);
|
|
if (in_array($locale, $supportedLocales)) {
|
|
return $locale;
|
|
}
|
|
}
|
|
|
|
// 3. 기본값 반환
|
|
return config('app.locale', 'ko');
|
|
}
|
|
|
|
/**
|
|
* 요청에서 사용자를 가져옵니다.
|
|
*
|
|
* 이 미들웨어는 auth:sanctum보다 먼저 실행되므로
|
|
* Auth::check()가 작동하지 않을 수 있습니다.
|
|
* 따라서 Bearer 토큰을 직접 파싱하여 사용자를 가져옵니다.
|
|
*
|
|
* @param Request $request HTTP 요청
|
|
* @return User|null 사용자 또는 null
|
|
*/
|
|
private function resolveUser(Request $request): ?User
|
|
{
|
|
// 이미 인증된 경우 (세션 기반 인증)
|
|
if (Auth::check()) {
|
|
return Auth::user();
|
|
}
|
|
|
|
// Bearer 토큰이 있는 경우 (API 토큰 인증)
|
|
$bearerToken = $request->bearerToken();
|
|
if ($bearerToken) {
|
|
$token = PersonalAccessToken::findToken($bearerToken);
|
|
|
|
// 만료된 토큰은 인증되지 않은 것으로 취급 (guest 로케일 폴백).
|
|
// OptionalSanctumMiddleware 와 동일한 만료 검사를 적용한다.
|
|
if ($token && $token->expires_at && $token->expires_at->isPast()) {
|
|
return null;
|
|
}
|
|
|
|
if ($token && $token->tokenable instanceof User) {
|
|
return $token->tokenable;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Accept-Language 헤더를 파싱하여 선호 언어를 추출합니다.
|
|
*
|
|
* @param string $acceptLanguage Accept-Language 헤더 값
|
|
* @return string 추출된 언어 코드 (기본값: config('app.locale'))
|
|
*/
|
|
private function parseAcceptLanguage(string $acceptLanguage): string
|
|
{
|
|
$supportedLocales = config('app.supported_locales', ['ko', 'en']);
|
|
$languages = explode(',', $acceptLanguage);
|
|
|
|
foreach ($languages as $language) {
|
|
$lang = trim(explode(';', $language)[0]);
|
|
|
|
// 언어-지역 형태에서 언어만 추출 (예: ko-KR -> ko)
|
|
if (strpos($lang, '-') !== false) {
|
|
$lang = explode('-', $lang)[0];
|
|
}
|
|
|
|
if (in_array($lang, $supportedLocales)) {
|
|
return $lang;
|
|
}
|
|
}
|
|
|
|
return config('app.locale', 'ko');
|
|
}
|
|
}
|