@@ -89,32 +89,11 @@ export function pullDockerImage(onProgress: (msg: string) => void): Promise<void
8989 } ) ;
9090}
9191
92- // ── 포트 충돌 대응 후보 포트 목록 ────────────────────────────
92+ // ── 포트 후보 목록 ────────────────────────────────────────────
93+ // Windows Hyper-V/WSL2 환경에서 5432가 자주 예약되므로 5433부터 시작.
94+ // 고정 범위를 모두 실패하면 15432 대역으로 넘어간다.
9395
94- const PORT_CANDIDATES = [ 5432 , 5433 , 5434 , 5435 ] ;
95-
96- /**
97- * 이미 실행 중인 컨테이너가 없는 포트를 순서대로 반환.
98- * 모두 사용 중이면 기본 포트를 그대로 사용 (Docker가 에러를 낸다).
99- */
100- async function pickAvailablePort ( ) : Promise < number > {
101- for ( const port of PORT_CANDIDATES ) {
102- try {
103- // 해당 포트를 점유한 컨테이너가 없는지 확인
104- const { stdout } = await execFileAsync ( 'docker' , [
105- 'ps' ,
106- '--filter' ,
107- `publish=${ port } ` ,
108- '--format' ,
109- '{{.ID}}' ,
110- ] ) ;
111- if ( ! stdout . trim ( ) ) return port ;
112- } catch {
113- return port ;
114- }
115- }
116- return DEFAULT_PORT ;
117- }
96+ const PORT_CANDIDATES = [ 5433 , 5434 , 5435 , 5436 , 5437 , 15432 , 15433 , 15434 ] ;
11897
11998// ── PostgreSQL 컨테이너 프로비저닝 ───────────────────────────
12099
@@ -124,29 +103,58 @@ export interface ProvisionResult {
124103}
125104
126105export async function provisionPostgresContainer ( ) : Promise < ProvisionResult > {
106+ // stopped 상태로 남아있는 동일 이름 컨테이너 제거 (이전 실패 잔여물)
107+ const existing = await getContainerStatus ( ) ;
108+ if ( existing === 'stopped' ) {
109+ await execFileAsync ( 'docker' , [ 'rm' , CONTAINER_NAME ] ) ;
110+ }
111+
127112 const password = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
128- const port = await pickAvailablePort ( ) ;
129-
130- await execFileAsync ( 'docker' , [
131- 'run' ,
132- '-d' ,
133- '--name' ,
134- CONTAINER_NAME ,
135- '--restart' ,
136- 'unless-stopped' ,
137- '-e' ,
138- `POSTGRES_USER=${ POSTGRES_USER } ` ,
139- '-e' ,
140- `POSTGRES_PASSWORD=${ password } ` ,
141- '-e' ,
142- `POSTGRES_DB=${ POSTGRES_DB } ` ,
143- '-p' ,
144- `${ port } :5432` ,
145- POSTGRES_IMAGE ,
146- ] ) ;
147-
148- const connectionUrl = `postgresql://${ POSTGRES_USER } :${ password } @localhost:${ port } /${ POSTGRES_DB } ` ;
149- return { connectionUrl, port } ;
113+
114+ // 포트 후보를 순서대로 시도 — Docker 실행 결과로 직접 판별한다.
115+ // WSL2 ↔ Windows 네트워크 스택 차이로 Node.js 바인딩 테스트는 신뢰할 수 없으므로
116+ // docker run을 실제로 시도하고 포트 충돌 에러 시 다음 포트로 넘어간다.
117+ for ( const port of PORT_CANDIDATES ) {
118+ try {
119+ await execFileAsync ( 'docker' , [
120+ 'run' ,
121+ '-d' ,
122+ '--name' ,
123+ CONTAINER_NAME ,
124+ '--restart' ,
125+ 'unless-stopped' ,
126+ '-e' ,
127+ `POSTGRES_USER=${ POSTGRES_USER } ` ,
128+ '-e' ,
129+ `POSTGRES_PASSWORD=${ password } ` ,
130+ '-e' ,
131+ `POSTGRES_DB=${ POSTGRES_DB } ` ,
132+ '-p' ,
133+ `${ port } :5432` ,
134+ POSTGRES_IMAGE ,
135+ ] ) ;
136+ // 성공
137+ const connectionUrl = `postgresql://${ POSTGRES_USER } :${ password } @localhost:${ port } /${ POSTGRES_DB } ` ;
138+ return { connectionUrl, port } ;
139+ } catch ( err ) {
140+ const msg = ( err as Error ) . message ?? '' ;
141+ const isPortError =
142+ msg . includes ( 'ports are not available' ) ||
143+ msg . includes ( 'address already in use' ) ||
144+ msg . includes ( 'bind:' ) ||
145+ msg . includes ( 'access permissions' ) ;
146+
147+ if ( ! isPortError ) throw err ; // 포트 문제가 아니면 즉시 실패
148+
149+ // 포트 문제 → 컨테이너가 절반만 생성됐을 수 있으므로 정리 후 다음 포트 시도
150+ try { await execFileAsync ( 'docker' , [ 'rm' , '-f' , CONTAINER_NAME ] ) ; } catch { /* ignore */ }
151+ }
152+ }
153+
154+ throw new Error (
155+ `후보 포트(${ PORT_CANDIDATES . join ( ', ' ) } ) 모두 사용 불가. ` +
156+ 'PostgreSQL 연결 URL을 직접 입력해주세요.' ,
157+ ) ;
150158}
151159
152160// ── PostgreSQL 준비 대기 (연결 폴링) ─────────────────────────
0 commit comments