작업 효율을 높이려고 뽀모도로(25분 집중, 5분 휴식) 기법을 자주 사용합니다. 그런데 막상 PC나 태블릿에 띄워두고 쓸만한 타이머 앱을 찾다 보면 거슬리는 점이 꽤 많습니다.

화면을 가리는 배너 광고, 굳이 설치를 요구하는 무거운 앱, 그리고 데스크 환경과 어울리지 않는 조잡한 UI들. 복잡한 기능 없이 딱 '시간 측정'이라는 본질에만 충실하면서 보기에 깔끔한 타이머가 필요해서 직접 만들었습니다.

[Focus Timer 바로가기] 👉 ( https://wontae.kr/projects/project-pomodoro.html )

 

주요 특징

설치나 로그인 없이 브라우저에서 바로 실행되는 가벼운 웹앱입니다.

1. 직관적인 배경색 전환 현재 상태를 굳이 글자로 읽지 않아도 직관적으로 인지할 수 있도록 모드별로 배경색이 바뀝니다.

  • 포모도로 (25분): 뇌를 깨우는 테라코타 레드
  • 짧은 휴식 (5분): 눈이 편안한 뮤트 틸
  • 긴 휴식 (15분): 차분한 스틸 블루

2. 데스크테리어에 최적화된 미니멀 UI 글래스모피즘(Glassmorphism) 기반의 반투명 패널과 가독성 높은 폰트만 남겼습니다. 서브 모니터나 아이패드에서 전체화면(F11)으로 띄워두면 깔끔한 탁상시계처럼 활용할 수 있습니다.

3. 시각적 알림 (화면 깜빡임) 소리 알림에 의존하면 무음 모드이거나 노이즈 캔슬링 이어폰을 끼고 있을 때 타이머가 끝난 걸 놓치기 쉽습니다. 시간이 다 되면 화면 전체가 부드럽게 깜빡이도록 시각적 알림 기능을 추가했습니다.

4. 광고 제로, 무설치 HTML/CSS/JS로만 가볍게 작성된 정적 웹페이지입니다. 광고나 트래커가 없어 쾌적합니다.

사용 팁

웹 브라우저 기반이므로 스마트폰이나 태블릿(아이패드 등)에서 사용할 때는 브라우저 메뉴의 '홈 화면에 추가' 기능을 사용해 보세요. 주소창이 사라지고 마치 네이티브 앱처럼 전체화면으로 단독 실행됩니다.

집중력이 떨어져서 작업 사이클 관리가 필요하신 분들은 즐겨찾기 해두고 활용해 보시기 바랍니다.

 

기획 설명 : https://wontae.kr/projects/project-timer.html

https://wontae.kr/projects/project-pomodoro.html

 

레트로 & 사이버펑크 감성, '아스키 아트(ASCII)'의 귀환

최근 웹 디자인이나 그래픽 포스터, 뮤직비디오 등에서 텍스트 기호로 이미지를 표현하는 '아스키 아트' 기법을 자주 보셨을 겁니다. 특유의 너디하면서도 힙한 Y2K, 매트릭스 감성을 내기에 이만한 소스가 없죠.

그런데 내 이미지나 영상을 고퀄리티 아스키아트로 바꾸려고 구글링을 해보면... 마치 2005년에 만들어진 듯한 광고 투성이 사이트들뿐입니다. 해상도 조절도 엉망이고, 결과물은 뭉개지기 일쑤죠.

"내가 원하는 컬러, 원하는 기호, 심지어 영상까지 실시간으로 아스키 변환을 할 수는 없을까?"

이런 답답함에서 출발해, 오직 크리에이터들의 시각적 쾌감을 위해 만든 웹 기반 프로페셔널 아스키 렌더러를 소개합니다.

ASCII Studio : 브라우저 안의 매트릭스

설치? 가입? 필요 없습니다. 브라우저 창 하나면 내 컴퓨터가 화려한 사이버펑크 터미널로 변신합니다.

👉 ASCII Studio Pro 바로가기 https://wontae.kr/projects/project-ascii.html

 

시중의 흔한 변환기와 무엇이 다른가요?

1. 사진은 기본,  동영상과 실시간 웹캠까지 변환! 단순한 정지 이미지를 넘어 MP4, WebM 동영상은 물론 노트북 웹캠 화면까지 실시간(Real-time) 60프레임 아스키 아트로 변환합니다. 웹캠을 켜두고 화면 앞에서 움직여보세요. 

2. 포토샵 부럽지 않은 디테일 제어 (디더링 & 컬러 보정)  아스키 아트의 생명은 명암비입니다. 단순 변환을 넘어 해상도, 폰트 크기, 대비(Contrast), 채도를 슬라이더로 미세 조정할 수 있습니다. 특히 빈티지한 그라데이션을 표현하는 '플로이드-스타인버그(Floyd-Steinberg) 디더링' 알고리즘을 탑재해 텍스트만으로도 디테일을 구현합니다.

3. 5가지 터미널 테마와 특수효과 

  • 테마: Hacker Green(매트릭스), Terminal Amber(레트로 앰버), Cyber Cyan, Monochrome 등
  • 특수효과: 텍스트가 빛나는 Text Glow, 브라운관 모니터의 감성을 그대로 살린 CRT Scanlines 효과를 클릭 한 번에 적용할 수 있습니다.
  • 원본 이미지의 색상을 텍스트에 입히는 'Original Colors' 모드도 지원합니다.

4. 클릭 한 번에 깔끔한 내보내기 (Export) 만들어진 멋진 결과물은 투명/배경 PNG 이미지, 순수 TXT 파일, 그리고 스타일이 입혀진 HTML 코드로 즉시 다운로드할 수 있습니다. 내 웹사이트나 포트폴리오에 바로 복붙해서 써먹어 보세요!

💡 이런 분들에게 강력 추천합니다!

  • 힙한 레트로/사이버펑크 디자인 소스가 필요한 그래픽 디자이너
  • 터미널 감성, 코딩 감성의 개인 웹사이트를 꾸미고 싶은 프론트엔드 개발자
  • 그냥 웹캠 켜놓고 멍때리며 시각적 즐거움을 느끼고 싶은 모든 분들

 

 

https://wontae.kr/projects/project-5.html

 

Architect Exam App | kimwontae

1. Project Overview 건축기사 실기 시험 준비를 위한 스마트 학습 웹 애플리케이션입니다. 기존의 종이 책 학습 방식에서 벗어나, Firebase Firestore를 활용한 실시간 데이터 동기화와 AI 기반 키워드 매

wontae.kr

 

무거운 건축기사 10개년 실기 기출문제집, 언제까지 들고 다니실 건가요?

건축기사 시험 준비의 꽃은 뭐니 뭐니 해도 '기출문제 뺑뺑이(과년도 풀이)'입니다. 그런데 다들 어떻게 공부하고 계신가요?

아마 책가방의 절반을 차지하는 두껍고 무거운 '10개년 기출문제집'을 낑낑대며 들고 다니시거나, 출퇴근/통학 길에 스마트폰으로 보려고 구형 사이트에 접속하고 계실 겁니다.

기존 방식의 끔찍한 단점들

  • 종이책: 너무 무겁습니다. 대중교통에서 펼쳐보는 건 불가능에 가깝고, 한 번 풀고 나면 지우개로 지우기도 벅찹니다.
  • 기존 CBT 사이트: 모바일 호환성이 떨어져서 화면을 이리저리 확대/축소해야 합니다. 게다가 화면 절반을 가리는 배너 광고 때문에 실수로 광고를 클릭하기 일쑤죠. UX/UI는 마치 2005년에 멈춰있는 것 같습니다.

"왜 건축기사 기출문제는 깔끔하고 편하게 풀 수 있는 곳이 없을까?" 디자인 실무를 하는 입장에서 이 불편함을 견딜 수 없어, 직접 모바일에 최적화된 웹앱을 만들어 버렸습니다.

건축기사 합격 메이커 : 모바일 최적화 기출문제 웹앱

군더더기 없이 오직 '문제 풀이'에만 집중할 수 있도록 기획하고 개발한 [건축기사 기출문제 실기 웹앱]을 소개합니다. 별도의 앱 설치 없이 브라우저에서 바로 실행됩니다.

👉 [건축기사 웹앱 바로가기 ( https://engineer-architecture.web.app/ )]

기존 사이트들과 무엇이 다른가요?

1. 압도적으로 깔끔한 UI/UX (광고 Zero) 사용자의 시선을 분산시키는 배너 광고나 불필요한 요소를 모두 제거했습니다. 마치 최신 트렌드의 에듀테크(EdTech) 어플을 사용하는 것처럼, 세련되고 직관적인 인터페이스에서 온전히 문제에만 집중할 수 있습니다.

2. 출퇴근길 10분 컷! 완벽한 모바일 반응형  지하철이나 버스 안에서 한 손으로 편하게 풀 수 있도록 모바일 화면에 완벽하게 최적화했습니다. 글씨가 작아서 확대할 필요도, 스크롤을 이리저리 옮길 필요도 없습니다. 자투리 시간을 활용한 '스마트한 기출 뺑뺑이'가 가능합니다.

3. 즉각적인 피드백과 직관적인 네비게이션  답을 체크하면 직관적으로 정답/오답 여부를 확인할 수 있으며, 과목별 진행 상황을 한눈에 파악할 수 있도록 네비게이션을 구성했습니다.

건축학도 & 실무자 여러분, 이제 스마트하게 합격하세요!

건축기사 시험은 시간 싸움입니다. 무거운 책을 펼치기 위해 카페나 도서관에 갈 필요 없이, 언제 어디서나 스마트폰만 꺼내면 그곳이 바로 시험장이 됩니다.

지금 바로 접속해서 테스트해보세요. (즐겨찾기나 홈 화면에 바로가기를 추가해두면 앱처럼 사용할 수 있습니다!)

 

 

주변에 건축기사를 준비하는 동기, 선후배들이 있다면 이 링크를 꼭 공유해 주세요. 모두의 합격을 기원합니다

https://engineer-architecture.web.app/

 

건축기사 실기 기출문제 학습

 

engineer-architecture.web.app

 

 

당신의 이메일 마지막 줄, 어떻게 생겼나요?

도면, 제안서, 렌더링 파일... 건축 설계나 디자인 실무를 하다 보면 하루에도 수십 통씩 클라이언트나 협력업체와 이메일을 주고받게 됩니다.

정성스럽게 작성한 메일 본문과 첨부파일 아래, 당신의 서명은 어떤 모습인가요? 혹시 아직 비어있나요??

 

막상 예쁘게 디자인해서 서명을 적용하려고 하면 큰 장벽에 부딪힙니다. 아웃룩(Outlook), 지메일(Gmail), 네이버 메일 등 이메일 클라이언트마다 화면을 읽어내는 방식이 전부 다르기 때문입니다.

우리가 흔히 아는 모던한 웹 코딩(Flexbox, Grid)은 이메일에서 작동하지 않습니다. 안 깨지는 서명을 만들려면 2000년대 초반에나 쓰던 구시대적인 'HTML 표(Table)' 코딩과 인라인 CSS를 써야만 하죠. 디자이너가 이 복잡한 코드의 늪에서 시간을 낭비할 수는 없습니다.

그래서 정보 전달에만 집중할 수 있도록, 웹 기반 이메일 서명 제너레이터를 만들었습니다.


Signature Studio : 이메일 서명 제작 툴

[Signature Studio]를 소개합니다. 개발 지식이 전혀 없어도, 몇 번의 클릭만으로 완벽하게 반응하고 절대 깨지지 않는 이메일 서명을 만들 수 있습니다.

👉 [Signature Studio 바로가기 (https://wontae.kr/projects/project-card.html)]

실무자 취향 저격 핵심 기능

1. 조형미를 고려한 프로페셔널 레이아웃 황금비 분할, 미니멀 스택, 중앙 정렬 등 시각적 비례가 훌륭한 4가지 템플릿을 제공합니다. 폰트(산스/명조)와 브랜드 컬러를 내 스튜디오의 아이덴티티에 맞게 커스터마이징 해보세요.

2. 다크모드 완벽 대비 (다크모드 시뮬레이터) 요즘은 스마트폰과 PC 모두 다크모드를 많이 씁니다. 흰색 배경의 로고를 무작정 넣었다가 수신자의 다크모드 화면에서 시커먼 네모 박스처럼 보여 당황하신 적 있으시죠? 툴 내부에 '다크모드 뷰어'를 탑재하여, 내 로고와 텍스트가 어두운 배경에서도 가독성이 좋은지 배포 전에 미리 검증할 수 있습니다.

3. 귀찮은 코딩 제로, 원클릭 복사 복잡한 HTML 코드를 볼 필요가 없습니다. 좌측에서 정보를 입력하고, 우측의 실시간 캔버스에서 결과를 확인한 뒤 [명함 복사] 버튼을 누르세요. 그대로 내 이메일 환경설정 '서명 추가' 란에 Ctrl+V (붙여넣기) 하시면 끝입니다.


사용 방법 (1분 컷)

  1. 위 링크를 통해 Signature Studio에 접속합니다.
  2. 프로필 이미지(로고) URL과 이름, 직책, 스튜디오 이름 등을 입력합니다. (인스타그램 등 소셜 링크도 추가 가능!)
  3. 원하는 레이아웃과 컬러를 선택합니다.
  4. 다크모드 시뮬레이터로 안전성을 확인한 후, [명함 복사] 버튼을 누릅니다.
  5. 아웃룩, 지메일, 애플 메일 등의 서명 설정 창에 붙여넣기 하면 완료!

(※ 팁: 이미지는 깨짐 방지를 위해 웹에 호스팅된 URL 형태(http://...)로 넣는 것을 권장합니다.)

작은 디테일이 이메일의 인상을 바꿉니다

잘 디자인된 이메일 서명은 클라이언트에게 '이 스튜디오는 보이지 않는 곳까지 꼼꼼하게 신경 쓰는구나'라는 무언의 신뢰를 줍니다.

매번 메일을 보낼 때마다 아쉬움이 남았다면, 이번 기회에 [Signature Studio]로 여러분만의 멋진 디지털 명함을 세팅해 보세요!

개발 비하인드 & 포트폴리오 보기 100% 바닐라 JS와 HTML Table 구조로 이 툴을 어떻게 설계했는지 궁금하시다면, 제 포트폴리오 사이트에서 기술적인 내용을 확인해 보실 수 있습니다.  https://wontae.kr/projects/project-sign.html

 

 

블로그 포스팅 효율을 높이기 위해 개발한 웹 유틸리티입니다.
구글 드라이브의 공유 링크를 웹사이트 게시용 태그(HTML, Markdown)로 즉시 변환합니다.

 

 피땀 흘려 만든 내 작업물, 업로드 하다가 지치신 적 없나요?

건축 설계나 디자인 프로젝트가 하나 끝나면, 도면, 다이어그램, 렌더링, 패널 등 수십 장의 이미지가 쏟아져 나옵니다. 이걸 개인 웹사이트나 티스토리, 노션 포트폴리오에 정리하려고 구글 드라이브에 올려두었는데... 막상 링크를 복사해서 붙여넣으면 이미지가 엑스박스(X)로 깨지는 경험, 다들 한 번쯤 있으실 겁니다.

구글 드라이브에서 제공하는 '링크 복사'는 이미지 파일 원본의 주소가 아니라 '뷰어(Viewer) 페이지'의 주소이기 때문입니다.

 기존의 작업 방식

이걸 웹에 이미지로 띄우려면,

  1. 구글 드라이브 공유 링크를 복사한다.
  2. 링크 중간에 있는 알 수 없는 복잡한 ID 문자열만 마우스로 조심스레 드래그해서 복사한다.
  3. https://lh3.googleusercontent.com/d/ 뒤에 그 ID를 붙여넣는다.
  4. 이걸 이미지 30장이면 30번 반복한다... (이러다 밤샙니다 🫠)

반복적이고 소모적인 이 과정이 너무 답답해서, 직접 문제를 해결할 수 있는 웹 유틸리티를 만들었습니다.


 구글 드라이브 이미지 직링크 일괄 변환기

제가 직접 기획하고 개발한 **[Google Drive Link Converter]**를 소개합니다. 별도의 설치나 가입 없이 웹에서 바로 사용할 수 있는 무료 툴입니다.

👉 [변환기 바로가기 링크 (https://wontae.kr/projects/projects-transfer-demo.html)]

 디자인 실무자를 위한 핵심 기능

1. 수십 장의 링크, 한 번에 일괄 변환 (Batch Process) 한 장씩 바꿀 필요가 없습니다. 구글 드라이브에서 복사한 여러 개의 링크를 텍스트 박스에 통째로 붙여넣으면, 각 링크의 고유 ID만 정규표현식으로 쏙쏙 뽑아내어 한 번에 직링크로 바꿔줍니다.

2. 포트폴리오 제작에 최적화된 포맷 지원 단순 URL뿐만 아니라, 상황에 맞게 바로 복붙할 수 있도록 코드를 생성해 줍니다.

  • HTML ( <img> ): 개인 웹사이트나 티스토리 HTML 모드에서 사용
  • Markdown ( ! [ ] ( ) ): 노션(Notion), 벨로그(Velog), 깃허브(GitHub) 작성 시 사용
  • 이미지 Alt 텍스트(자동 넘버링)와 Width 속성도 툴 안에서 클릭 몇 번으로 설정할 수 있습니다.

3. 서버에 데이터를 전송하지 않아 보안상 안전합니다.


 어떻게 쓰나요?

  1. 구글 드라이브에서 공유할 이미지들의 권한을 '링크가 있는 모든 사용자에게 공개'로 변경합니다.
  2. 이미지들의 링크를 복사하여 변환기 창에 붙여넣습니다. (여러 줄 붙여넣기 가능)
  3. 원하는 포맷(HTML/Markdown)을 선택하고 [변환하기] 버튼을 누릅니다.
  4. 완성된 코드를 복사해서 내 포트폴리오나 블로그에 붙여넣으면 끝!

 디자이너의 시간은 소중하니까요

반복되는 단순 노동은 시스템에 맡기고, 우리는 공간의 비례와 디자인의 디테일을 고민하는 데 시간을 써야 합니다. 웹 포트폴리오를 준비하는 건축학도, 디자이너 분들의 야근을 조금이나마 줄여주는 유용한 도구가 되길 바랍니다!

🌐 개발 비하인드 & 포트폴리오 보기 이 툴이 어떻게 만들어졌는지 기술적인 과정이 궁금하시다면, 제 포트폴리오 사이트에서 확인해 보실 수 있습니다.

 

 

 

스케치업 내보내기용 화면 해상도 고정 루비를 만들었습니다.

 

 

 

 

 1. 설치방법

스케치업 열기 > Extension > Extension Manger 에서

좌측하단의 Install Extension > 다운로드한 .rbz파일 열기 > Extension Manger상에서 Enabled 적용되어있는지 확인

 

 

 2. 사용위치

스케치업 열기 > Extension  > Crop window export tool

 

 

 3. 다운로드 ( rbz를 다운받아서 압축을 풀지 마시고 설치하세요 )

https://wontae.kr/wt_cropwindowrb.html

 

 

 

 

1. main.py

import uvicorn
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from rembg import remove
from PIL import Image
import io
import os
import datetime
import uuid # 고유 파일명 생성을 위해 추가
import sqlite3
import logging # 로깅 추가

# --- 설정 ---
app = FastAPI()

# 환경 변수 사용 (또는 기본값)
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
PROCESSED_DIR = os.getenv("PROCESSED_DIR", "processed")
DB_NAME = os.getenv("DB_NAME", "image_db.sqlite")
MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", "10")) # 최대 파일 크기 10MB
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"} # 허용된 확장자

# 로깅 설정
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 업로드/처리된 이미지를 저장할 폴더 생성
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(PROCESSED_DIR, exist_ok=True)

# --- DB 설정 ---
def init_db():
    conn = None
    try:
        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS images (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            original_filename TEXT NOT NULL, # 원본 업로드 시의 파일명 (클라이언트 기준)
            original_server_path TEXT NOT NULL, # 서버에 저장된 원본 파일의 경로
            processed_server_path TEXT NOT NULL, # 서버에 저장된 처리된 파일의 경로
            processed_url TEXT NOT NULL, # 클라이언트가 접근할 수 있는 처리된 이미지 URL
            uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        ''')
        conn.commit()
        logger.info(f"Database {DB_NAME} initialized.")
    except sqlite3.Error as e:
        logger.error(f"Error initializing database: {e}")
    finally:
        if conn:
            conn.close()

init_db()

# --- 헬퍼 함수 ---
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# 이미지 파일의 매직 넘버(시그니처) 검사 (일부 주요 이미지 형식만)
def is_valid_image(file_content):
    if file_content.startswith(b'\x89PNG\r\n\x1a\n'): # PNG
        return True
    if file_content.startswith(b'\xFF\xD8\xFF'): # JPEG
        return True
    if file_content.startswith(b'GIF87a') or file_content.startswith(b'GIF89a'): # GIF
        return True
    if file_content.startswith(b'RIFF') and b'WEBP' in file_content: # WEBP (간단 검사)
        return True
    return False

# --- API 엔드포인트 ---
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/processed_images", StaticFiles(directory=PROCESSED_DIR), name="processed_images") # URL 경로 변경

@app.post("/process-image/")
async def process_image_endpoint(file: UploadFile = File(...)):
    if not file.filename:
        raise HTTPException(status_code=400, detail="파일 이름이 없습니다.")

    # 1. 파일 확장자 검사
    if not allowed_file(file.filename):
        logger.warning(f"업로드 거부: 허용되지 않는 확장자 - {file.filename}")
        raise HTTPException(status_code=400, detail="허용되지 않는 이미지 파일 형식입니다. (png, jpg, jpeg, gif, webp만 가능)")

    # 2. 파일 크기 제한
    file_content = await file.read()
    if len(file_content) > MAX_FILE_SIZE_MB * 1024 * 1024:
        logger.warning(f"업로드 거부: 파일 크기 초과 - {file.filename} ({len(file_content) / (1024*1024):.2f} MB)")
        raise HTTPException(status_code=413, detail=f"파일 크기는 {MAX_FILE_SIZE_MB}MB를 초과할 수 없습니다.")
    
    # 3. 파일 시그니처 검사
    if not is_valid_image(file_content):
        logger.warning(f"업로드 거부: 유효하지 않은 이미지 시그니처 - {file.filename}")
        raise HTTPException(status_code=400, detail="유효하지 않은 이미지 파일입니다.")

    # 4. 고유한 파일 이름 생성 (UUID 사용)
    file_extension = file.filename.rsplit('.', 1)[1].lower()
    unique_id = str(uuid.uuid4())
    original_server_filename = f"{unique_id}_original.{file_extension}"
    processed_server_filename = f"{unique_id}_processed.png" # rembg는 PNG로 출력하므로 고정

    original_server_path = os.path.join(UPLOAD_DIR, original_server_filename)
    processed_server_path = os.path.join(PROCESSED_DIR, processed_server_filename)
    
    # 클라이언트가 접근할 수 있는 URL (StaticFiles 마운트 경로와 일치)
    processed_client_url = f"/processed_images/{processed_server_filename}"

    # 5. 원본 이미지 저장
    try:
        with open(original_server_path, "wb") as f:
            f.write(file_content)
        logger.info(f"원본 이미지 저장: {original_server_path}")
    except IOError as e:
        logger.error(f"원본 이미지 저장 오류: {e}")
        raise HTTPException(status_code=500, detail="원본 이미지 저장에 실패했습니다.")

    # 6. 이미지 배경 제거
    try:
        output_bytes = remove(file_content)
        output_image = Image.open(io.BytesIO(output_bytes))
        
        # 7. 결과 이미지 저장
        output_image.save(processed_server_path)
        logger.info(f"처리된 이미지 저장: {processed_server_path}")

        # 8. 데이터베이스에 파일 정보 저장
        conn = None
        try:
            conn = sqlite3.connect(DB_NAME)
            cursor = conn.cursor()
            cursor.execute(
                "INSERT INTO images (original_filename, original_server_path, processed_server_path, processed_url) VALUES (?, ?, ?, ?)",
                (file.filename, original_server_path, processed_server_path, processed_client_url)
            )
            conn.commit()
            logger.info(f"DB에 이미지 정보 저장 완료: {processed_client_url}")
        except sqlite3.Error as db_e:
            logger.error(f"DB 저장 오류: {db_e}")
            raise HTTPException(status_code=500, detail="데이터베이스 저장에 실패했습니다.")
        finally:
            if conn:
                conn.close()

        # 9. 클라이언트에 결과 반환
        return {
            "message": "처리 완료",
            "original_url": original_server_path, # 개발 편의상 제공, 실제 서비스에서는 보안상 내부 경로 노출 지양
            "processed_url": processed_client_url
        }
        
    except Exception as e:
        logger.error(f"이미지 처리 중 오류 발생: {e}", exc_info=True)
        # 이미 원본 파일이 저장되었을 수 있으므로 정리 (선택 사항, 복잡해질 수 있음)
        # if os.path.exists(original_server_path): os.remove(original_server_path)
        raise HTTPException(status_code=500, detail="이미지 처리 중 예상치 못한 오류가 발생했습니다.")

@app.get("/", response_class=HTMLResponse)
async def read_index():
    with open('static/index.html', 'r', encoding='utf-8') as f:
        return HTMLResponse(content=f.read())

# --- 서버 실행 ---
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

+ Recent posts