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 코드로 즉시 다운로드할 수 있습니다. 내 웹사이트나 포트폴리오에 바로 복붙해서 써먹어 보세요!
1. 압도적으로 깔끔한 UI/UX (광고 Zero) 사용자의 시선을 분산시키는 배너 광고나 불필요한 요소를 모두 제거했습니다. 마치 최신 트렌드의 에듀테크(EdTech) 어플을 사용하는 것처럼, 세련되고 직관적인 인터페이스에서 온전히 문제에만 집중할 수 있습니다.
2. 출퇴근길 10분 컷! 완벽한 모바일 반응형 지하철이나 버스 안에서 한 손으로 편하게 풀 수 있도록 모바일 화면에 완벽하게 최적화했습니다. 글씨가 작아서 확대할 필요도, 스크롤을 이리저리 옮길 필요도 없습니다. 자투리 시간을 활용한 '스마트한 기출 뺑뺑이'가 가능합니다.
3. 즉각적인 피드백과 직관적인 네비게이션 답을 체크하면 직관적으로 정답/오답 여부를 확인할 수 있으며, 과목별 진행 상황을 한눈에 파악할 수 있도록 네비게이션을 구성했습니다.
건축학도 & 실무자 여러분, 이제 스마트하게 합격하세요!
건축기사 시험은 시간 싸움입니다. 무거운 책을 펼치기 위해 카페나 도서관에 갈 필요 없이, 언제 어디서나 스마트폰만 꺼내면 그곳이 바로 시험장이 됩니다.
지금 바로 접속해서 테스트해보세요. (즐겨찾기나 홈 화면에 바로가기를 추가해두면 앱처럼 사용할 수 있습니다!)
주변에 건축기사를 준비하는 동기, 선후배들이 있다면 이 링크를 꼭 공유해 주세요. 모두의 합격을 기원합니다
도면, 제안서, 렌더링 파일... 건축 설계나 디자인 실무를 하다 보면 하루에도 수십 통씩 클라이언트나 협력업체와 이메일을 주고받게 됩니다.
정성스럽게 작성한 메일 본문과 첨부파일 아래, 당신의 서명은 어떤 모습인가요? 혹시 아직 비어있나요??
막상 예쁘게 디자인해서 서명을 적용하려고 하면 큰 장벽에 부딪힙니다. 아웃룩(Outlook), 지메일(Gmail), 네이버 메일 등 이메일 클라이언트마다 화면을 읽어내는 방식이 전부 다르기 때문입니다.
우리가 흔히 아는 모던한 웹 코딩(Flexbox, Grid)은 이메일에서 작동하지 않습니다. 안 깨지는 서명을 만들려면 2000년대 초반에나 쓰던 구시대적인 'HTML 표(Table)' 코딩과 인라인 CSS를 써야만 하죠. 디자이너가 이 복잡한 코드의 늪에서 시간을 낭비할 수는 없습니다.
그래서 정보 전달에만 집중할 수 있도록, 웹 기반 이메일 서명 제너레이터를 만들었습니다.
Signature Studio : 이메일 서명 제작 툴
[Signature Studio]를 소개합니다. 개발 지식이 전혀 없어도, 몇 번의 클릭만으로 완벽하게 반응하고 절대 깨지지 않는 이메일 서명을 만들 수 있습니다.
1. 조형미를 고려한 프로페셔널 레이아웃 황금비 분할, 미니멀 스택, 중앙 정렬 등 시각적 비례가 훌륭한 4가지 템플릿을 제공합니다. 폰트(산스/명조)와 브랜드 컬러를 내 스튜디오의 아이덴티티에 맞게 커스터마이징 해보세요.
2. 다크모드 완벽 대비 (다크모드 시뮬레이터) 요즘은 스마트폰과 PC 모두 다크모드를 많이 씁니다. 흰색 배경의 로고를 무작정 넣었다가 수신자의 다크모드 화면에서 시커먼 네모 박스처럼 보여 당황하신 적 있으시죠? 툴 내부에 '다크모드 뷰어'를 탑재하여, 내 로고와 텍스트가 어두운 배경에서도 가독성이 좋은지 배포 전에 미리 검증할 수 있습니다.
3. 귀찮은 코딩 제로, 원클릭 복사 복잡한 HTML 코드를 볼 필요가 없습니다. 좌측에서 정보를 입력하고, 우측의 실시간 캔버스에서 결과를 확인한 뒤 [명함 복사] 버튼을 누르세요. 그대로 내 이메일 환경설정 '서명 추가' 란에 Ctrl+V (붙여넣기) 하시면 끝입니다.
사용 방법 (1분 컷)
위 링크를 통해 Signature Studio에 접속합니다.
프로필 이미지(로고) URL과 이름, 직책, 스튜디오 이름 등을 입력합니다. (인스타그램 등 소셜 링크도 추가 가능!)
원하는 레이아웃과 컬러를 선택합니다.
다크모드 시뮬레이터로 안전성을 확인한 후, [명함 복사] 버튼을 누릅니다.
아웃룩, 지메일, 애플 메일 등의 서명 설정 창에 붙여넣기 하면 완료!
(※ 팁: 이미지는 깨짐 방지를 위해 웹에 호스팅된 URL 형태(http://...)로 넣는 것을 권장합니다.)
작은 디테일이 이메일의 인상을 바꿉니다
잘 디자인된 이메일 서명은 클라이언트에게 '이 스튜디오는 보이지 않는 곳까지 꼼꼼하게 신경 쓰는구나'라는 무언의 신뢰를 줍니다.
매번 메일을 보낼 때마다 아쉬움이 남았다면, 이번 기회에 [Signature Studio]로 여러분만의 멋진 디지털 명함을 세팅해 보세요!
건축 설계나 디자인 프로젝트가 하나 끝나면, 도면, 다이어그램, 렌더링, 패널 등 수십 장의 이미지가 쏟아져 나옵니다. 이걸 개인 웹사이트나 티스토리, 노션 포트폴리오에 정리하려고 구글 드라이브에 올려두었는데... 막상 링크를 복사해서 붙여넣으면 이미지가 엑스박스(X)로 깨지는 경험, 다들 한 번쯤 있으실 겁니다.
구글 드라이브에서 제공하는 '링크 복사'는 이미지 파일 원본의 주소가 아니라 '뷰어(Viewer) 페이지'의 주소이기 때문입니다.
기존의 작업 방식
이걸 웹에 이미지로 띄우려면,
구글 드라이브 공유 링크를 복사한다.
링크 중간에 있는 알 수 없는 복잡한 ID 문자열만 마우스로 조심스레 드래그해서 복사한다.
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"} # 허용된 확장자
# 업로드/처리된 이미지를 저장할 폴더 생성 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로 출력하므로 고정
# 클라이언트가 접근할 수 있는 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)