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:
@ -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"
|
||||||
|
|||||||
@ -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 ---
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user