11import os
22import subprocess
3- import httpx
43import sys
4+ import httpx
55from flask import Flask , request , jsonify
66from openai import OpenAI
77from dotenv import load_dotenv
8+ from pathlib import Path
9+ import shlex
810
9- # .env 파일 로드
11+ # .env 로드
1012load_dotenv ()
1113
12- # 환경변수 확인
14+ # ==== OpenAI 클라이언트 (키 없으면 비활성) ====
1315OPENAI_API_KEY = os .getenv ("OPENAI_API_KEY" )
14- print (f"✅ Loaded OPENAI_API_KEY: { OPENAI_API_KEY [:5 ]} ..." ) # Key 일부만 출력
16+ if OPENAI_API_KEY :
17+ print ("🔐 OPENAI_API_KEY loaded" )
18+ else :
19+ print ("⚠️ OPENAI_API_KEY not set; OpenAI features disabled" )
1520
16- # httpx Client 설정
1721http_client = httpx .Client (timeout = 30.0 )
18-
19- # OpenAI Client 생성
20- client = OpenAI (
21- api_key = OPENAI_API_KEY ,
22- http_client = http_client
23- )
22+ client = OpenAI (api_key = OPENAI_API_KEY , http_client = http_client ) if OPENAI_API_KEY else None
2423
2524app = Flask (__name__ )
2625
27- @ app . route ( '/' )
28- def index ():
29- return 'DebugVisual Python Compiler Server is running!'
26+ # 실행 작업 디렉터리
27+ TMP_DIR = Path ( "/usr/src/app/code" )
28+ TMP_DIR . mkdir ( parents = True , exist_ok = True )
3029
31- @app .route ('/test' , methods = ['POST' ])
32- def test ():
33- print ("✅ /test 진입됨" )
34- return "pong"
30+ MAX_TIME = int (os .getenv ("EXEC_TIMEOUT_SEC" , "10" )) # 실행 타임아웃
31+ PY_BIN = ["python" , "-X" , "utf8" ] # 파이썬 실행 커맨드
3532
33+ # ========== 공통 유틸 ==========
34+ def run_proc (cmd , * , cwd = None , stdin_data = "" , timeout = MAX_TIME ):
35+ """명령 실행 유틸. stdout/stderr/returncode 반환."""
36+ try :
37+ print (f"▶️ run: { shlex .join (cmd )} (cwd={ cwd } )" )
38+ p = subprocess .run (
39+ cmd ,
40+ input = (stdin_data or "" ).encode ("utf-8" ),
41+ cwd = cwd ,
42+ stdout = subprocess .PIPE ,
43+ stderr = subprocess .PIPE ,
44+ timeout = timeout ,
45+ )
46+ return p .stdout .decode ("utf-8" , "ignore" ), p .stderr .decode ("utf-8" , "ignore" ), p .returncode , None
47+ except subprocess .TimeoutExpired :
48+ return "" , "" , - 1 , "⏰ 실행 시간이 초과되었습니다."
49+
50+ def safe_print (msg ):
51+ try :
52+ sys .stdout .buffer .write ((str (msg ) + "\n " ).encode ("utf-8" ))
53+ sys .stdout .flush ()
54+ except Exception :
55+ fallback_msg = str (msg ).encode ("utf-8" , "backslashreplace" ).decode ("ascii" , "ignore" )
56+ print (f"[safe_print fallback] { fallback_msg } " , flush = True )
57+
58+ def normalize_lang (lang : str ) -> str :
59+ if not lang :
60+ return "python"
61+ l = lang .lower ()
62+ if l in {"py" , "python3" }:
63+ return "python"
64+ if l .startswith ("java" ):
65+ return "java"
66+ if l in {"c" }:
67+ return "c"
68+ return l
69+
70+ # ========== 웹 훅/헬스/CORS ==========
3671@app .before_request
3772def log_request_info ():
3873 print (f"📡 요청 URL: { request .url } " )
3974 print (f"📡 요청 메서드: { request .method } " )
4075
4176@app .after_request
42- def cors_headers (response ):
43- response .headers ['Access-Control-Allow-Origin' ] = '*'
44- response .headers ['Access-Control-Allow-Headers' ] = 'Content-Type'
45- response .headers ['Access-Control-Allow-Methods' ] = 'POST, OPTIONS'
46- return response
77+ def cors_headers (resp ):
78+ resp .headers ["Access-Control-Allow-Origin" ] = "*"
79+ resp .headers ["Access-Control-Allow-Headers" ] = "Content-Type"
80+ resp .headers ["Access-Control-Allow-Methods" ] = "POST, OPTIONS"
81+ return resp
82+
83+ @app .get ("/" )
84+ def index ():
85+ return "DebugVisual Python Compiler Server is running!"
86+
87+ @app .get ("/healthz" )
88+ def healthz ():
89+ return "OK" , 200
4790
48- @app .route ('/echo' , methods = ['POST' ])
91+ @app .post ("/test" )
92+ def test ():
93+ print ("✅ /test 진입됨" )
94+ return "pong"
95+
96+ @app .post ("/echo" )
4997def echo ():
5098 try :
5199 data = request .get_json (force = True )
@@ -55,111 +103,84 @@ def echo():
55103 print ("❌ JSON 파싱 실패:" , e )
56104 return "Invalid JSON" , 400
57105
58- # 공통 코드 실행 함수
59- def execute_code (code , input_data , lang ):
60- base_dir = '/home/ec2-user/DebugVisual_Spike/server/code'
61- flask_dir = '/usr/src/app/code'
62- os .makedirs (base_dir , exist_ok = True )
63-
64- file_map = {
65- 'c' : ('main.c' , 'c-compiler' ),
66- 'python' : ('main.py' , 'python-compiler' ),
67- 'java' : ('Main.java' , 'java-compiler' ),
68- }
69-
70- if lang not in file_map :
71- return None , f"❌ 지원하지 않는 언어입니다: { lang } "
72-
73- filename , image = file_map [lang ]
74- code_path = os .path .join (flask_dir , filename )
75- input_path = os .path .join (flask_dir , 'input.txt' )
76-
77- with open (code_path , 'w' ) as f :
78- f .write (code )
79-
80- with open (input_path , 'w' ) as f :
81- f .write (input_data )
82-
83- if lang == 'python' :
84- docker_cmd = [
85- 'docker' , 'run' , '--rm' ,
86- '-v' , f'{ base_dir } :/code' ,
87- '-w' , '/code' , image ,
88- 'python' , filename
89- ]
90- elif lang == 'java' :
91- docker_cmd = [
92- 'docker' , 'run' , '--rm' ,
93- '-v' , f'{ base_dir } :/code' ,
94- '-w' , '/code' , image ,
95- 'sh' , '-c' , 'javac Main.java && java Main'
96- ]
97- elif lang == 'c' :
98- docker_cmd = [
99- 'docker' , 'run' , '--rm' ,
100- '-v' , f'{ base_dir } :/code' ,
101- '-w' , '/code' , image ,
102- 'sh' , '-c' , 'gcc main.c -o program && ./program'
103- ]
104-
105- print ("🐳 Docker 실행 명령어:" , ' ' .join (docker_cmd ))
106-
107- try :
108- result = subprocess .run (
109- docker_cmd ,
110- stdout = subprocess .PIPE ,
111- stderr = subprocess .PIPE ,
112- text = True ,
113- timeout = 10
114- )
115- except subprocess .TimeoutExpired :
116- return {
117- "stdout" : "" ,
118- "stderr" : "" ,
119- "exitCode" : - 1 ,
120- "success" : False ,
121- "error" : "⏰ 실행 시간이 초과되었습니다."
122- }, None
123-
124- return result , None
125-
126- @app .route ('/run' , methods = ['POST' ])
106+ # ========== 언어별 실행 ==========
107+ def exec_python (code : str , stdin_data : str ):
108+ src = TMP_DIR / "main.py"
109+ src .write_text (code or "" , encoding = "utf-8" )
110+ return run_proc ([* PY_BIN , str (src )], cwd = TMP_DIR , stdin_data = stdin_data )
111+
112+ def exec_c (code : str , stdin_data : str ):
113+ src = TMP_DIR / "main.c"
114+ bin_path = TMP_DIR / "program"
115+ src .write_text (code or "" , encoding = "utf-8" )
116+
117+ # 컴파일
118+ out , err , rc , to = run_proc (["gcc" , "-O2" , "-std=c11" , str (src ), "-o" , str (bin_path )], cwd = TMP_DIR )
119+ if to : # 타임아웃
120+ return "" , to , - 1 , to
121+ if rc != 0 :
122+ return out , err , rc , None
123+
124+ # 실행
125+ return run_proc ([str (bin_path )], cwd = TMP_DIR , stdin_data = stdin_data )
126+
127+ def exec_java (code : str , stdin_data : str ):
128+ # 클래스명은 Main으로 고정
129+ src = TMP_DIR / "Main.java"
130+ src .write_text (code or "" , encoding = "utf-8" )
131+
132+ # 컴파일
133+ out , err , rc , to = run_proc (["javac" , str (src )], cwd = TMP_DIR )
134+ if to :
135+ return "" , to , - 1 , to
136+ if rc != 0 :
137+ return out , err , rc , None
138+
139+ # 실행 (CLASSPATH = TMP_DIR)
140+ return run_proc (["java" , "-cp" , str (TMP_DIR ), "Main" ], cwd = TMP_DIR , stdin_data = stdin_data )
141+
142+ def execute_code (code : str , input_data : str , lang : str ):
143+ lang = normalize_lang (lang )
144+ if lang == "python" :
145+ return exec_python (code , input_data )
146+ if lang == "c" :
147+ return exec_c (code , input_data )
148+ if lang == "java" :
149+ return exec_java (code , input_data )
150+ return "" , f"❌ 지원하지 않는 언어입니다: { lang } " , 1 , None
151+
152+ # ========== /run ==========
153+ @app .route ("/run" , methods = ["POST" , "OPTIONS" ])
127154def run_code ():
128- print ("📥 /run 요청 수신됨" )
155+ if request .method == "OPTIONS" :
156+ return jsonify ({"ok" : True }), 200
129157
158+ print ("📥 /run 요청 수신됨" )
130159 try :
131160 data = request .get_json (force = True )
132161 print ("✅ JSON 파싱 성공:" , data )
133162 except Exception as e :
134163 print (f"❌ JSON 파싱 실패: { e } " )
135164 return jsonify ({"error" : "JSON 파싱 오류" , "message" : str (e )}), 400
136165
137- code = data .get ('code' , '' )
138- input_data = data .get ('input' , '' )
139- lang = data .get ('lang' , '' )
140-
141- result , error = execute_code (code , input_data , lang )
166+ # lang / language 둘 다 지원, 기본값 python
167+ lang = data .get ("lang" ) or data .get ("language" ) or "python"
168+ code = data .get ("code" , "" )
169+ stdin_data = data .get ("input" , "" ) or data .get ("stdin" , "" )
142170
143- if error :
144- return jsonify ({"error" : error }), 400
171+ out , err , rc , timeout_msg = execute_code (code , stdin_data , lang )
172+ if timeout_msg :
173+ return jsonify ({"stdout" : out , "stderr" : timeout_msg , "exitCode" : - 1 , "success" : False }), 400
145174
146175 return jsonify ({
147- "stdout" : result .stdout ,
148- "stderr" : result .stderr ,
149- "exitCode" : result .returncode ,
150- "success" : result .returncode == 0
151- }), 200 if result .returncode == 0 else 400
152-
153-
154- def safe_print (msg ):
155- try :
156- sys .stdout .buffer .write ((str (msg ) + '\n ' ).encode ('utf-8' ))
157- sys .stdout .flush ()
158- except Exception as e :
159- fallback_msg = str (msg ).encode ('utf-8' , 'backslashreplace' ).decode ('ascii' , 'ignore' )
160- print (f"[safe_print fallback] { fallback_msg } " , flush = True )
161-
162- @app .route ('/visualize' , methods = ['POST' ])
176+ "stdout" : out ,
177+ "stderr" : err ,
178+ "exitCode" : rc ,
179+ "success" : rc == 0
180+ }), 200 if rc == 0 else 400
181+
182+ # ========== /visualize ==========
183+ @app .post ("/visualize" )
163184def visualize_code ():
164185 print ("📥 /visualize 요청 수신됨" , flush = True )
165186
@@ -170,46 +191,43 @@ def visualize_code():
170191 print (f"❌ JSON 파싱 실패: { e } " , flush = True )
171192 return jsonify ({"error" : "JSON 파싱 오류" , "message" : str (e )}), 400
172193
173- code = data .get ('code' , '' )
174- input_data = data .get ('input' , '' )
175- lang = data .get ('lang' , '' )
176-
177- result , error = execute_code (code , input_data , lang )
194+ lang = data .get ("lang" ) or data .get ("language" ) or "python"
195+ code = data .get ("code" , "" )
196+ stdin_data = data .get ("input" , "" ) or data .get ("stdin" , "" )
197+
198+ out , err , rc , timeout_msg = execute_code (code , stdin_data , lang )
199+ if timeout_msg :
200+ return jsonify ({"error" : timeout_msg }), 400
201+ # GPT 호출
202+ gpt_response = None
203+ if client is None :
204+ print ("⚠️ OpenAI 비활성: OPENAI_API_KEY 미설정" , flush = True )
205+ gpt_response = "OpenAI disabled"
206+ else :
207+ gpt_prompt = f"{ code } "
208+ try :
209+ completion = client .chat .completions .create (
210+ model = "gpt-4o" ,
211+ messages = [
212+ {"role" : "system" ,
213+ "content" : "사용자 코드에 대한 단계별 시각화 JSON만 출력하세요." },
214+ {"role" : "user" , "content" : gpt_prompt },
215+ ],
216+ )
217+ gpt_response = completion .choices [0 ].message .content
218+ safe_print (f"✅ 최종 프론트 전달용 GPT 응답: { repr (gpt_response )} " )
219+ except Exception as e :
220+ print (f"❌ GPT 응답 호출 실패: { e } " , flush = True )
221+ gpt_response = "GPT 응답 호출 실패"
178222
179- if error :
180- return jsonify ({"error" : error }), 400
181-
182- # GPT 호출용 프롬프트 구성
183- gpt_prompt = f"{ code } "
184-
185- try :
186- completion = client .chat .completions .create (
187- model = "gpt-4o" ,
188- messages = [
189- {
190- "role" : "system" ,
191- "content" : "당신은 **코드 실행 시각화 JSON 생성기**입니다. 사용자가 언어(lang), 코드(code), 입력값(input)을 주면 아래 스키마에 맞춰서 **모든 연산, 비교, 대입, 스왑, 함수 호출, 반복문, 조건문, 재귀 호출, 자료구조 변화 등을 단계별로** 시각화 가능한 JSON을 **정확하고 완전하게** 생성하세요.\n \n **출력 JSON 스키마**:\n {\n \" lang\" : \" 언어\" ,\n \" TimeComplexity\" : \" 시간복잡도(Big O)\" ,\n \" SpaceComplexity\" : \" 공간복잡도(Big O)\" ,\n \" input\" : \" 입력값(없으면 빈 문자열)\" ,\n \" variables\" : [\n { \" name\" : \" 변수명\" , \" type\" : \" 자료형|array|graph|heap|linkedList|bst\" , \" initialValue\" : 값, \" currentValue\" : 값 },\n …\n ],\n \" functions\" : [\n { \" name\" : \" 함수명\" , \" params\" : [\" param1\" , …], \" called\" : 호출횟수 },\n …\n ],\n \" steps\" : [\n {\n \" line\" : 소스코드_행번호,\n \" description\" : \" 이 단계에서 일어난 일\" ,\n \" changes\" : [ { \" variable\" : \" 변수명\" , \" before\" : 이전값, \" after\" : 이후값 }, … ],\n \" stack\" : [ { \" function\" : \" 함수명\" , \" params\" : [값들] }, … ], (선택)\n \" loop\" : { \" type\" : \" for|while|do-while\" , \" index\" : 현재반복인덱스, \" total\" : 총반복횟수 }, (선택)\n \" condition\" : { \" expression\" : \" 조건식\" , \" result\" : true|false }, (선택)\n \" dataStructure\" : { (선택)\n \" type\" : \" array|linkedList|bst|heap|graph\" ,\n \" nodes\" : [ \" 0\" , \" 1\" , … ] | [ { \" id\" : \" 0\" , \" value\" : 값, \" links\" : [\" 1\" , …] }, … ],\n \" edges\" : [[\" 0\" ,\" 1\" ],[\" 1\" ,\" 2\" ],…], // graph 전용\n \" adjacencyMatrix\" : [[0,1],[1,0],…] // graph 전용\n }\n },\n …\n ]\n }\n \n ✅ 단계별로 dataStructure 객체를 **반드시** 포함하여 자료구조 상태를 **정확히** 기록하세요.\n ✅ graph의 경우 반드시 nodes, edges, adjacencyMatrix를 **모두 포함**해야 합니다.\n ✅ 생략이나 … 없이 **모든 단계의 흐름과 변수/자료구조 변화**를 상세히 출력하세요.\n ✅ HTML 시각화 코드와 호환되도록 구조를 유지하며, 다른 설명이나 텍스트는 **절대로 추가하지 마세요**.\n ✅ **올바른 JSON만** 출력하세요."
192- },
193- {"role" : "user" , "content" : gpt_prompt }
194- ]
195- )
196- gpt_response = completion .choices [0 ].message .content
197- safe_print (f"✅ 최종 프론트 전달용 GPT 응답: { repr (gpt_response )} " )
198-
199- except Exception as e :
200- print (f"❌ GPT 응답 호출 실패: { e } " , flush = True )
201- gpt_response = "GPT 응답 호출 실패"
202-
203- # 프론트로 응답 전송
204223 return jsonify ({
205- "stdout" : result . stdout ,
206- "stderr" : result . stderr ,
224+ "stdout" : out ,
225+ "stderr" : err ,
207226 "ast" : gpt_response ,
208- "exitCode" : result .returncode ,
209- "success" : result .returncode == 0
210- }), 200 if result .returncode == 0 else 400
211-
212-
227+ "exitCode" : rc ,
228+ "success" : rc == 0
229+ }), 200 if rc == 0 else 400
213230
214- if __name__ == '__main__' :
215- app .run (host = "0.0.0.0" , port = 5050 )
231+ if __name__ == "__main__" :
232+ port = int (os .getenv ("PORT" , "5050" ))
233+ app .run (host = "0.0.0.0" , port = port )
0 commit comments