feat: 사이트 검사 동시 검사 수 설정 추가

- 기본값 2→4로 변경, 사용자가 [1, 2, 4, 8] 중 선택 가능
- 백엔드: concurrency 파라미터 추가 (API → 서비스 → Semaphore)
- 프론트: 드롭다운에 "동시 검사 수" 옵션 UI 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-13 17:44:22 +09:00
parent c440f1c332
commit 1e50b72fd8
6 changed files with 48 additions and 9 deletions

View File

@ -24,7 +24,7 @@ class Settings(BaseSettings):
# Site inspection # Site inspection
SITE_MAX_PAGES: int = 500 SITE_MAX_PAGES: int = 500
SITE_MAX_DEPTH: int = 2 SITE_MAX_DEPTH: int = 2
SITE_CONCURRENCY: int = 2 SITE_CONCURRENCY: int = 4
# Application # Application
PROJECT_NAME: str = "Web Inspector API" PROJECT_NAME: str = "Web Inspector API"

View File

@ -30,6 +30,7 @@ class StartSiteInspectionRequest(BaseModel):
url: HttpUrl url: HttpUrl
max_pages: int = Field(default=20, ge=0, le=500, description="최대 크롤링 페이지 수 (0=무제한)") max_pages: int = Field(default=20, ge=0, le=500, description="최대 크롤링 페이지 수 (0=무제한)")
max_depth: int = Field(default=2, ge=1, le=3, description="최대 크롤링 깊이") max_depth: int = Field(default=2, ge=1, le=3, description="최대 크롤링 깊이")
concurrency: int = Field(default=4, ge=1, le=8, description="동시 검사 수")
class InspectPageRequest(BaseModel): class InspectPageRequest(BaseModel):
@ -67,6 +68,7 @@ class SiteInspectionConfig(BaseModel):
"""사이트 검사 설정.""" """사이트 검사 설정."""
max_pages: int = 20 max_pages: int = 20
max_depth: int = 2 max_depth: int = 2
concurrency: int = 4
# --- Response Models --- # --- Response Models ---

View File

@ -66,6 +66,7 @@ async def start_site_inspection(request: StartSiteInspectionRequest):
url=url, url=url,
max_pages=request.max_pages, max_pages=request.max_pages,
max_depth=request.max_depth, max_depth=request.max_depth,
concurrency=request.concurrency,
) )
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
raise HTTPException( raise HTTPException(

View File

@ -49,6 +49,7 @@ class SiteInspectionService:
url: str, url: str,
max_pages: int = 20, max_pages: int = 20,
max_depth: int = 2, max_depth: int = 2,
concurrency: int = 4,
) -> str: ) -> str:
""" """
Start a site-wide inspection. Start a site-wide inspection.
@ -65,6 +66,7 @@ class SiteInspectionService:
if max_pages > 0: if max_pages > 0:
max_pages = min(max_pages, settings.SITE_MAX_PAGES) max_pages = min(max_pages, settings.SITE_MAX_PAGES)
max_depth = min(max_depth, settings.SITE_MAX_DEPTH) max_depth = min(max_depth, settings.SITE_MAX_DEPTH)
concurrency = min(concurrency, settings.SITE_CONCURRENCY)
site_inspection_id = str(uuid.uuid4()) site_inspection_id = str(uuid.uuid4())
parsed = urlparse(url) parsed = urlparse(url)
@ -81,6 +83,7 @@ class SiteInspectionService:
"config": { "config": {
"max_pages": max_pages, "max_pages": max_pages,
"max_depth": max_depth, "max_depth": max_depth,
"concurrency": concurrency,
}, },
"discovered_pages": [], "discovered_pages": [],
"aggregate_scores": None, "aggregate_scores": None,
@ -88,13 +91,13 @@ class SiteInspectionService:
await self.db.site_inspections.insert_one(doc) await self.db.site_inspections.insert_one(doc)
logger.info( logger.info(
"Site inspection started: id=%s, url=%s, max_pages=%d, max_depth=%d", "Site inspection started: id=%s, url=%s, max_pages=%d, max_depth=%d, concurrency=%d",
site_inspection_id, url, max_pages, max_depth, site_inspection_id, url, max_pages, max_depth, concurrency,
) )
# Launch background task # Launch background task
asyncio.create_task( asyncio.create_task(
self._crawl_and_inspect(site_inspection_id, url, max_pages, max_depth) self._crawl_and_inspect(site_inspection_id, url, max_pages, max_depth, concurrency)
) )
return site_inspection_id return site_inspection_id
@ -268,6 +271,7 @@ class SiteInspectionService:
url: str, url: str,
max_pages: int, max_pages: int,
max_depth: int, max_depth: int,
concurrency: int = 4,
) -> None: ) -> None:
""" """
Background task that runs in two phases: Background task that runs in two phases:
@ -349,8 +353,7 @@ class SiteInspectionService:
# ============================== # ==============================
logger.info("Phase 2 (inspection) started: %s", site_inspection_id) logger.info("Phase 2 (inspection) started: %s", site_inspection_id)
settings = get_settings() semaphore = asyncio.Semaphore(concurrency)
semaphore = asyncio.Semaphore(settings.SITE_CONCURRENCY)
tasks = [ tasks = [
self._inspect_page_with_semaphore( self._inspect_page_with_semaphore(

View File

@ -18,6 +18,9 @@ const MAX_PAGES_OPTIONS = [10, 20, 50, 0] as const;
/** 크롤링 깊이 옵션 */ /** 크롤링 깊이 옵션 */
const MAX_DEPTH_OPTIONS = [1, 2, 3] as const; const MAX_DEPTH_OPTIONS = [1, 2, 3] as const;
/** 동시 검사 수 옵션 */
const CONCURRENCY_OPTIONS = [1, 2, 4, 8] as const;
export function UrlInputForm() { export function UrlInputForm() {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -26,6 +29,7 @@ export function UrlInputForm() {
const [showSiteOptions, setShowSiteOptions] = useState(false); const [showSiteOptions, setShowSiteOptions] = useState(false);
const [maxPages, setMaxPages] = useState<number>(20); const [maxPages, setMaxPages] = useState<number>(20);
const [maxDepth, setMaxDepth] = useState<number>(2); const [maxDepth, setMaxDepth] = useState<number>(2);
const [concurrency, setConcurrency] = useState<number>(4);
const router = useRouter(); const router = useRouter();
const { setInspection } = useInspectionStore(); const { setInspection } = useInspectionStore();
const { setSiteInspection } = useSiteInspectionStore(); const { setSiteInspection } = useSiteInspectionStore();
@ -101,7 +105,8 @@ export function UrlInputForm() {
const response = await api.startSiteInspection( const response = await api.startSiteInspection(
trimmedUrl, trimmedUrl,
maxPages, maxPages,
maxDepth maxDepth,
concurrency
); );
setSiteInspection(response.site_inspection_id, trimmedUrl); setSiteInspection(response.site_inspection_id, trimmedUrl);
router.push( router.push(
@ -225,7 +230,7 @@ export function UrlInputForm() {
</div> </div>
{/* 크롤링 깊이 */} {/* 크롤링 깊이 */}
<div className="mb-4"> <div className="mb-3">
<label className="text-xs text-muted-foreground mb-1.5 block"> <label className="text-xs text-muted-foreground mb-1.5 block">
</label> </label>
@ -250,6 +255,32 @@ export function UrlInputForm() {
</div> </div>
</div> </div>
{/* 동시 검사 수 */}
<div className="mb-4">
<label className="text-xs text-muted-foreground mb-1.5 block">
</label>
<div className="flex gap-2">
{CONCURRENCY_OPTIONS.map((option) => (
<Button
key={option}
type="button"
variant={
concurrency === option ? "default" : "outline"
}
size="sm"
className={cn(
"flex-1",
concurrency === option && "pointer-events-none"
)}
onClick={() => setConcurrency(option)}
>
{option}
</Button>
))}
</div>
</div>
{/* 사이트 검사 시작 버튼 */} {/* 사이트 검사 시작 버튼 */}
<Button <Button
type="button" type="button"

View File

@ -157,7 +157,8 @@ class ApiClient {
async startSiteInspection( async startSiteInspection(
url: string, url: string,
maxPages?: number, maxPages?: number,
maxDepth?: number maxDepth?: number,
concurrency?: number
): Promise<StartSiteInspectionResponse> { ): Promise<StartSiteInspectionResponse> {
return this.request("/api/site-inspections", { return this.request("/api/site-inspections", {
method: "POST", method: "POST",
@ -165,6 +166,7 @@ class ApiClient {
url, url,
max_pages: maxPages, max_pages: maxPages,
max_depth: maxDepth, max_depth: maxDepth,
concurrency: concurrency,
}), }),
}); });
} }