#fff 205개의 스레드 ✕ 해제
이온디
이온디 1주 전
이온디 관리자 앱 — eond.com 운영 관리 버전1.0.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항첫 정식 서명 릴리스 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 이온디 관리자 앱 — eond.com 운영 관리 버전1.0.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항첫 정식 서명 릴리스 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
이온디 사용자 앱 — 마켓, 구매, 마이페이지 버전1.0.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항첫 정식 서명 릴리스 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 이온디 사용자 앱 — 마켓, 구매, 마이페이지 버전1.0.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항첫 정식 서명 릴리스 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
X(트위터) 스크랩 도구 버전1.4 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. X(트위터) 스크랩 도구 버전1.4 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
독서 기록 앱 버전2.9 (2026-04-02)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항카메라 촬영, 읽은날짜 편집, 업로드시 날짜편집 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 독서 기록 앱 버전2.9 (2026-04-02)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항카메라 촬영, 읽은날짜 편집, 업로드시 날짜편집 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
아이들 치아 관리 앱 (해찬·은찬) — 진료/치료/일일체크 기록 버전2.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항웹버전(Svelte)을 내장한 웹앱으로 재구성 — 오프라인 동작, 데이터는 기기에 저장 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 아이들 치아 관리 앱 (해찬·은찬) — 진료/치료/일일체크 기록 버전2.0 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항웹버전(Svelte)을 내장한 웹앱으로 재구성 — 오프라인 동작, 데이터는 기기에 저장 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
인스타그램·스레드·유튜브 피드 수집/큐레이션 (Flutter) 버전1.0.1 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항소스 복구 후 release 재빌드 (서명·경량화) 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 인스타그램·스레드·유튜브 피드 수집/큐레이션 (Flutter) 버전1.0.1 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항소스 복구 후 release 재빌드 (서명·경량화) 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
은행 입금 알림을 웹훅으로 전달 (웹매니저 연동) 버전2.2 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항카카오뱅크 알림 누락 보강, 알림 기록 검색·개별 삭제 추가 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다. 은행 입금 알림을 웹훅으로 전달 (웹매니저 연동) 버전2.2 (2026-06-11)플랫폼Android (이온디 툴박스에서 설치) 최근 변경사항카카오뱅크 알림 누락 보강, 알림 기록 검색·개별 삭제 추가 이온디 툴박스 받기 이 앱은 이온디 툴박스에서 설치·업데이트할 수 있습니다.
이온디
이온디 1주 전
이온디 앱 모음 — 설치와 업데이트를 한 곳에서 버전1.7 (2026-06-11)플랫폼Android (APK 직접 설치) 최근 변경사항전자책 탭 추가(다운로드), 하단 탭 4개(카탈로그/전자책/의뢰하기/도움말), 탭 무반응 버그 수정 APK 다운로드 툴박스를 설치하면 이온디의 모든 앱을 받고 자동 업데이트할 수 있습니다. 이온디 앱 모음 — 설치와 업데이트를 한 곳에서 버전1.7 (2026-06-11)플랫폼Android (APK 직접 설치) 최근 변경사항전자책 탭 추가(다운로드), 하단 탭 4개(카탈로그/전자책/의뢰하기/도움말), 탭 무반응 버그 수정 APK 다운로드 툴박스를 설치하면 이온디의 모든 앱을 받고 자동 업데이트할 수 있습니다.
이온디
이온디 1개월 전
시리즈 ← 이전 편 바이브코딩의 위험성 ③ — 사람은 잊지만 코드는 잊지 않는다 사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. — 시리즈 3편 / 회고와 안전망 설계 바이브코딩의 양면성 "바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와 의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄 모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나 for-loop을 외울 필요가 없다. 그 자리에 들… 시리즈 ← 이전 편 바이브코딩의 위험성 ③ — 사람은 잊지만 코드는 잊지 않는다 사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. — 시리즈 3편 / 회고와 안전망 설계 바이브코딩의 양면성 "바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와 의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄 모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나 for-loop을 외울 필요가 없다. 그 자리에 들어선 건 기획·아이디어·도메인 이해다. 좋은 시대가 온 게 맞다. 그런데 좋은 시대는 새로운 종류의 사고도 함께 들고 온다. 이번 사고가 바로 그 예시였다. 평범한 모델 파일 하나를 추가하면서, alembic env.py에 import 한 줄을 추가하지 않았다. 그것 하나로 1091줄짜리 폭탄 마이그레이션이 만들어졌다. 사람은 한 줄을 잊었을 뿐인데, 자동화 도구는 그 망각을 30개 테이블의 DROP 문으로 증폭시켰다. AI 도구든 alembic 같은 자동화 스크립트든, 자동화는 인간의 실수를 대신 봐주지 않는다. 오히려 증폭시킨다. "조심하자"는 답이 아니다 가장 흔한 사후 대처는 "다음부터 조심하자"다. 그리고 가장 자주 실패하는 대처도 "다음부터 조심하자"다. env.py에 import를 빠뜨렸을 때 누구도 일부러 그런 게 아니다. 새 모델 추가하느라 정신없는 와중에, env.py라는 한참 떨어진 파일을 동시에 떠올리는 게 어려웠을 뿐이다. 인간은 작업 중에 컨텍스트를 좁힌다. 그게 결함이 아니라 본성이다. 그래서 답은 사람의 주의력에 의존하지 않는 구조다. 인간이 잊을 수 있는 자리를 자동화로 채우는 것. 이번 사고에서 우리가 한 일은 사실 그 한 가지였다. for _info in pkgutil.iter_modules(_models_pkg.__path__): if _info.name.startswith("_") or _info.name == "base": continue importlib.import_module(f"app.models.{_info.name}") 이 6줄이 의미하는 바는 단순하다. 누구도 더 이상 import를 까먹을 수 없다. 모델 파일이 디렉터리에 있으면 자동으로 잡힌다. 잊을 수 있는 자리 자체를 없앤 것이다. 3중 방어 — 사고를 몇 단계에서 막을 것인가 좋은 시스템은 한 군데에서 막는 게 아니라, 여러 군데에서 같은 사고를 잡는다. 첫 번째 방어선이 뚫려도 두 번째가 있고, 두 번째가 뚫려도 세 번째가 있어야 한다. 이번 일을 계기로 마이그레이션 사고에 대한 3중 방어를 설계했다. 1차 방어 — 모델 메타데이터 자동 등록 방금 본 그것. 누락 자체가 일어나지 않도록 구조를 바꾼다. 2차 방어 — Pre-commit gate 혹시라도 1차가 뚫려서 위험한 마이그레이션이 만들어졌다면, commit 단계에서 차단한다. 신규 마이그레이션 파일에 drop_table이나 drop_index가 일정 개수 이상이면 git이 commit을 거부하도록 한다. # .git/hooks/pre-commit NEW_MIGRATIONS=$(git diff --cached --name-only --diff-filter=A | grep '^alembic/versions/.*\.py$') for f in $NEW_MIGRATIONS; do DROPS=$(grep -cE "drop_table|drop_index" "$f") if [ "$DROPS" -ge 3 ]; then echo "❌ $f 에 drop 작업이 $DROPS 건 있습니다. 의도된 변경인지 확인하고 --no-verify로 우회하세요." exit 1 fi done 3차 방어 — Pre-deploy gate 다른 사람이 다른 환경에서 commit한 게 production까지 흘러왔다고 해도, 배포 직전에 한 번 더 막는다. 배포 스크립트(server.sh)에서 alembic upgrade를 돌리기 전에 dry-run을 먼저 시킨다. SQL=$(alembic upgrade --sql current:head) if echo "$SQL" | grep -qE "DROP TABLE|TRUNCATE"; then echo "❌ 배포 중단 — 위험한 SQL 감지" exit 1 fi alembic upgrade head 이 세 단계 중 어느 한 곳도 사람의 주의력에 의존하지 않는다. 자고 있어도, 출근길에 정신없어도, 이미 쳤던 명령을 의식 없이 다시 쳐도 — 안 터진다. 백업 자동화와 binlog 이번 사고에서 가장 손발이 묶였던 순간은 binlog가 OFF라는 걸 확인했을 때였다. 점-시간 복구라는, 데이터베이스의 마지막 안전망이 처음부터 없었던 것이다. binlog는 MySQL의 변경 이력을 별도 파일에 기록하는 기능이다. 이게 켜져 있으면 "어제 14:50:59 시점으로 복구해줘"가 가능하다. 꺼져 있으면 그냥 마지막 백업 시점까지밖에 못 돌아간다. 켜는 건 한 줄이다. /etc/mysql/mysql.conf.d/mysqld.cnf에: log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 7 expire_logs_days = 7이 중요하다. 안 적으면 binlog가 무한히 쌓여서 디스크가 꽉 찬다. 7일이면 보통의 사고 추적엔 충분하고, 디스크 부담도 적다. 그리고 binlog가 있어도 풀 백업은 따로 필요하다. 복구는 "풀 백업 + 그 이후의 binlog" 조합으로 이뤄지기 때문이다. 풀 백업은 cron으로 자동화한다. # 매일 새벽 3시 백업 0 3 * * * /home/.../scripts/db_backup.sh >> /home/.../private/backup.log 2>&1 # 매일 새벽 3시 30분, 14일 이전 백업 자동 삭제 30 3 * * * find /home/.../private/ -name '*-*.sql.gz' -mtime +14 -delete 이 두 줄이면 매일 새벽 백업이 만들어지고, 14일 이전 백업이 자동 정리된다. 한 번 설정하면 잊고 살아도 된다. 디테일이 안전을 만든다 이번 작업에서 사소해 보이지만 중요했던 디테일들을 적어둔다. set -euo pipefail — 백업 스크립트 첫 줄. 한 줄이라도 실패하면 즉시 멈춘다. 이게 없으면 mysqldump가 중간에 실패해도 gzip은 부분 데이터를 압축하면서 "성공" 종료 코드를 반환할 수 있다. 부분 백업은 백업이 아니다. 이 한 줄이 그 차이를 만든다. --single-transaction — InnoDB에서 락 없이 일관된 스냅샷을 떠준다. production 트래픽에 영향을 거의 안 주면서 데이터 일관성을 보장. --routines --triggers --events — 이걸 빼먹으면 트리거·뷰·저장 프로시저가 백업에 안 들어간다. 복원 후에 알 수 없는 동작이 사라진 채로 운영되는 상황이 생긴다. OUT_DIR="$HOME/web/.../private" — 절대 경로 대신 $HOME을 쓰면 다른 서버나 다른 계정에서도 그대로 동작한다. 그리고 백업 위치가 웹에서 노출되지 않는 디렉터리인지 반드시 확인한다. chmod +x를 로컬에서 미리 — rsync는 파일 권한을 그대로 복사한다. 로컬에서 +x를 안 했으면 production에서 또 chmod를 해야 한다. 작은 일이지만, 매번 잊는 종류의 일이다. ~/.ssh/config 별칭 등록 — 매번 풀 도메인 치는 대신 ssh mars 한 단어로 접속. 작은 편의지만, 이런 작은 마찰이 쌓이면 보안 작업도 귀찮아진다. (NEUTRALIZED) 같은 명시적 표시 — 비운 마이그레이션의 docstring 첫 줄에 이렇게 넣어두면, dry-run 출력에도 그대로 떠서 "지금 처리되는 게 무력화된 파일이 맞다"는 즉각적인 시각 확인이 된다. 6개월 뒤의 자기 자신을 위한 친절. 이런 디테일들은 하나하나는 사소하지만, 사고 한가운데에서 마음을 가라앉히는 건 결국 이런 작은 확실성들이다. 다 끝난 뒤에 남은 것 이번 사고로 잃은 데이터는 단순 로그 정도였고 다시 채울 수 있는 수준이었다. 하지만 만약 폭탄이 발견되지 않은 채 다음 배포 때 터졌다면? 그땐 30개 테이블이 진짜로 사라졌을 것이다. 그중에는 단순 로그만 있는 게 아니었다 — 고객 관리, 마케팅 캠페인, 뉴스레터 구독자, 게시판 SEO 설정 같은, 다시 채우려면 외부 자료 없이는 불가능한 데이터들이 섞여 있었다. 운이 좋았다. 그런데 운이 좋았다는 건 시스템이 안전했다는 게 아니라, 단지 이번엔 누군가 일찍 알아챘다는 뜻일 뿐이다. 다음 사고는 늦게 발견될 수도 있다. 자동화의 시대에는, 사고가 너무 빨리 너무 광범위하게 퍼진다. 알아챈 시점엔 이미 늦은 경우가 많다. 그래서 우리는 안전망을 코드에 박아둔다. env.py의 자동 import, pre-commit hook, pre-deploy gate, 매일 백업, binlog. 사람은 잊지만 코드는 잊지 않으니까. 바이브코딩이 위험한 게 아니다. 안전망 없는 자동화가 위험한 것이다. 자연어로 개발하는 시대에, 우리가 새로 배워야 할 건 아마 코드 그 자체가 아니라 — 자동화의 사각지대를 미리 메우는 감각일 것이다. 당신의 바이브코딩은 안전한가요? AI에게 코드를 맡겼다면, 안전망도 같이 맡겼는지 한 번 점검해보세요. 7가지 항목으로 끝나는 무료 자가 진단. 바이브코딩 안전 체크리스트 받기 → 시리즈 끝. 다음에는 더 안전한 코드로 만나길.
이온디
이온디 1개월 전
시리즈 ← 이전 편 · 다음 편 → 바이브코딩의 위험성 ② — 범인은 autogenerate였다 1091줄짜리 시한폭탄을 만든 건 사람이 아니라 자동화 도구 자신이었다. — 시리즈 2편 / 진짜 원인 발견과 해체 작업 Alembic autogenerate가 그러는 이유 Alembic은 SQLAlchemy 기반 프로젝트의 마이그레이션 도구다. alembic revision --autogenerate라는 명령을 치면, 현재 모델 정의(SQLAlchemy Base.metadata)와 실제… 시리즈 ← 이전 편 · 다음 편 → 바이브코딩의 위험성 ② — 범인은 autogenerate였다 1091줄짜리 시한폭탄을 만든 건 사람이 아니라 자동화 도구 자신이었다. — 시리즈 2편 / 진짜 원인 발견과 해체 작업 Alembic autogenerate가 그러는 이유 Alembic은 SQLAlchemy 기반 프로젝트의 마이그레이션 도구다. alembic revision --autogenerate라는 명령을 치면, 현재 모델 정의(SQLAlchemy Base.metadata)와 실제 DB 스키마를 비교해서 그 차이를 마이그레이션 파일로 자동 생성해준다. 컬럼 추가, 인덱스 추가, 테이블 추가 같은 변경을 사람이 손으로 SQL을 적지 않아도 되게 해주는 편리한 도구다. 문제는 이 비교의 방향성이다. Alembic은 "모델에는 있는데 DB에 없는 것"을 추가 작업으로 인식하고, 동시에 "DB에는 있는데 모델에 없는 것"을 삭제 작업으로 인식한다. 후자가 함정이다. Base.metadata에 어떤 모델이 등록되지 않은 상태에서 autogenerate를 돌리면, 실제로는 코드 어딘가에 살아 있는 모델이라도 alembic 입장에선 "사라진 테이블"이 된다. 그러면 친절하게 op.drop_table('...')을 자동으로 만들어준다. alembic/env.py를 열어봤다. from app.models import document, member, comment, file, module, site_config, site from app.models import hosting_site, hosting_subscription, project, inquiry, member_profile from app.models import sale_product, order, revenue, settlement from app.models import project_issue, kakao_chat, project_billing, project_file, project_comment 손으로 적은 import가 21개. 그런데 app/models/ 디렉터리에는 모델 파일이 57개. 빠진 36개의 정체: audit_log.py bank_transaction.py blog_post.py client.py hosting_setup_log.py marketing.py notification_log.py spam.py wiki.py analytics_report.py module_group.py newsletter.py ... 폭탄 마이그레이션이 DROP하려던 그 테이블들이, 정확히 env.py에서 import 누락된 모델 파일들과 일치했다. .title { font-size: 22px; font-weight: 700; fill: #111; } .subtitle { font-size: 14px; fill: #666; } .label { font-size: 14px; fill: #222; font-weight: 500; } .num { font-size: 28px; font-weight: 700; } .small { font-size: 12px; fill: #555; } .bad { fill: #c83737; } .good { fill: #1f7a3f; } .arrow { stroke: #777; stroke-width: 1.6; fill: none; } .arrow-bad { stroke: #c83737; stroke-width: 1.8; fill: none; stroke-dasharray: 4 3; } .arrow-good { stroke: #1f7a3f; stroke-width: 1.8; fill: none; } .box { stroke: #ccc; stroke-width: 1; fill: #fff; rx: 8; } .box-bad { stroke: #e0a8a8; stroke-width: 1; fill: #fff5f5; rx: 8; } .box-good { stroke: #a8d4b8; stroke-width: 1; fill: #f3faf5; rx: 8; } .row-label{ font-size: 13px; fill: #888; font-weight: 600; letter-spacing: 1px; } env.py의 침묵하는 누락 손으로 적은 import 21개 vs 디렉터리에 실재하는 모델 57개 BEFORE — 사고 시점 app/models/ 57 실재 모델 파일 env.py 21 손으로 적은 import Base.metadata 21 등록된 테이블 autogenerate −36 DROP TABLE 자동 생성 metadata에 없는 36개 모델은 alembic 입장에서 "사라진 테이블"로 보임 → 1091줄짜리 폭탄 마이그레이션 파일이 자동 생성됨 AFTER — env.py 자동 import 적용 app/models/ 57 실재 모델 파일 env.py auto pkgutil.iter_modules Base.metadata 78 전체 자동 등록 autogenerate 0 false-DROP 차단 새 모델 파일을 추가해도 env.py에 손대지 않아도 됨 — 잊을 수 있는 자리 자체가 사라짐 env.py에 손으로 적은 21개 import vs 디렉터리에 실재하는 57개 모델 — 그 격차가 폭탄을 만든다. 원인 확정. 사람의 게으름이 아니라, 시스템 설계의 문제였다. 새 모델을 추가할 때마다 env.py에 한 줄을 손으로 더 적어야 하는 구조 자체가, 언젠가 누락이 생길 시한폭탄이었다. 백업이 가장 먼저 원인을 알았다고 해서 바로 수정 작업에 들어가면 안 된다. binlog가 꺼진 상태에서, 잘못된 한 줄이 더 큰 사고를 만들 수도 있다. 이번 작업의 모든 안전성은 현재 시점 백업 한 장에 달려 있었다. 백업 스크립트를 짜기 전에 한 가지 고려사항이 있었다 — eondcms는 트래픽이 꽤 있는 사이트라 카운터·통계·API 호출 로그 테이블 3개가 매우 크다. 이걸 그대로 풀 덤프하면 시간도 오래 걸리고 용량도 부담스럽다. 이 3개는 데이터는 빼고 스키마만 보존하기로 했다. 복원 후에도 빈 테이블 껍데기는 만들어져야 ORM이 INSERT를 시도할 때 에러가 나지 않으니까. 핵심 패턴은 단순하다. mysqldump를 두 번 호출해서 stdout을 이어붙인 뒤 한 번에 gzip으로 압축한다. 결과물은 단일 파일. { mysqldump --single-transaction --routines --triggers --events \ --default-character-set=utf8mb4 --hex-blob \ --ignore-table=$DB.xe_counter_log \ --ignore-table=$DB.xe_api_call_logs \ --ignore-table=$DB.xe_stats_log \ "$DB" mysqldump --no-data --default-character-set=utf8mb4 \ "$DB" xe_counter_log xe_api_call_logs xe_stats_log } | gzip > "$OUT_FILE" 스크립트 첫 줄에 set -euo pipefail을 박아두는 게 중요하다. 한 줄이라도 실패하면 즉시 멈추도록. 백업이 도중에 깨졌는데 "성공"이라고 착각하는 사고가 가장 흔하니까. 결과물은 압축 후 37MB. 압축 전 원본 기준 약 300~450MB 추정. 이걸 웹에서 절대 보이지 않는 디렉터리에 저장하는 것도 중요했다. 백업 파일에는 비밀번호 해시·세션 토큰·이메일 같은 민감정보가 그대로 들어 있어서, "private"이라는 이름의 디렉터리에 둔다고 해도 실제로 웹서버 설정이 막아주지 않으면 누구나 다운로드 가능하다. env.py 자동화 — 사람의 주의력에 의존하지 않기 이제 진짜 수정. env.py를 손으로 import 목록을 유지하는 방식이 사고의 근본 원인이라면, 답은 자동 import다. 새 모델 파일이 추가될 때마다 자동으로 등록되도록 바꾸면, 누구도 손으로 적는 걸 까먹을 수 없다. import importlib import pkgutil import app.models as _models_pkg for _info in pkgutil.iter_modules(_models_pkg.__path__): if _info.name.startswith("_") or _info.name == "base": continue importlib.import_module(f"app.models.{_info.name}") pkgutil.iter_modules는 패키지 안의 모든 모듈을 순회해주는 표준 라이브러리. 베이스 모듈만 제외하고 전부 import한다. 검증 결과 55개 모듈이 자동 로드되어 78개 테이블이 metadata에 등록되었다. 이전 21개 import로 잡지 못했던 36개 모델이 이번에 모두 합류했다. 이 변경 한 줄이 의미하는 바는 명확하다. 앞으로 누가 새 모델 파일을 만들어도 env.py를 손대지 않아도 된다. autogenerate가 잘못된 DROP을 만들 가능성이 구조적으로 차단된다. 폭탄 마이그레이션 무력화 남은 일은 1091줄짜리 폭탄을 어떻게 처리할 것인가였다. 선택지는 두 개: A. 파일 자체를 삭제 — 깔끔해 보이지만, alembic 입장에선 chain의 한 노드가 사라지는 것이라 후속 마이그레이션이 깨진다. B. 내용만 비우기 — revision과 down_revision 필드는 그대로 두고, upgrade()와 downgrade() 함수 본문을 pass로 교체한다. chain은 그대로, 동작만 무력화. B를 골랐다. 사고 경위와 신원 보존이 되는 주석을 헤더에 적어두고: """add_random_order_to_board_config (NEUTRALIZED) ⚠️ 이 마이그레이션은 의도적으로 비워졌습니다 (2026-04-28). 사고 경위: alembic env.py가 app/models/ 아래 일부 모델만 import하던 상태에서 --autogenerate 가 실행되어, 누락된 36개 모델이 "사라진 테이블"로 인식 → 30+ 테이블 DROP을 자동 생성한 1091줄짜리 폭탄 마이그레이션이 됨. """ def upgrade() -> None: pass def downgrade() -> None: pass 이 주석은 미래의 누군가가 — 6개월 뒤의 자기 자신을 포함해서 — "왜 이 파일이 비어있지?" 라고 물었을 때, 짧은 단서가 되어줄 것이다. 코드 자체가 자기 역사를 설명할 수 있어야 한다. 안전한 적용 절차 수정한 파일들을 production에 보내기 전, 한 가지 더 확인할 게 있었다. dry-run. alembic upgrade --sql은 offline 모드로 동작해서, DB에 연결조차 하지 않고 실행될 SQL을 텍스트로만 출력한다. 진짜 실행 전에 무엇이 production에서 일어날지 미리 보여주는 안전 기능이다. alembic upgrade --sql c3d4e5f6a1b2:head | grep -E "DROP TABLE|TRUNCATE" \ && echo "❌ 아직 위험" \ || echo "✅ no destructive ops" grep이 무언가 잡히면 → 위험. 아무것도 안 나오면 → 안전. 아주 단순하지만 마음 편한 검증이다. 처음 production에서 dry-run을 돌렸을 때는 DROP 문이 좌라락 출력되었다. 잠깐 가슴이 철렁했지만, 곧 이유를 알았다 — 우리가 로컬에서 비운 두 파일이 production에는 아직 안 갔던 것. rsync로 동기화 후 재실행: Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07, add_random_order_to_board_config (NEUTRALIZED) ✅ no destructive ops (NEUTRALIZED) 표시가 떠 있는 게 결정적 증거였다. production이 우리가 비운 새 파일을 정상적으로 읽고 있다는 뜻. 이제 진짜로 적용해도 안전하다. $ alembic upgrade head INFO Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07, add_random_order_to_board_config (NEUTRALIZED) $ alembic current a6ae466f8b07 (head) DB 변경 0건, 데이터 손실 0건, 다운타임 0초. alembic_version 테이블의 한 행만 갱신되었다. 폭탄 해체 완료. 다음 편 — ③ 바이브코딩의 위험과 안전망 — 사람은 잊지만 코드는 잊지 않는다
이온디
이온디 1개월 전
2025. 06. 17 초고 작성 개발자들을 위한 나라는 없다 누구나 개발자가 될 수 있는 시대, 진짜 개발자의 가치를 찾아서 PDF 다운로드 온라인으로 읽기 새벽 3시, 모니터 앞에서 에너지드링크를 마시며 버그와 씨름하던 그 시절이 있었다. ChatGPT가 내가 3일간 짠 코드를 5분 만에 뚝딱 만들어내는 걸 보고 나서, 문득 이런 생각이 들었다. "개발자들을 위한 나라… 2025. 06. 17 초고 작성 개발자들을 위한 나라는 없다 누구나 개발자가 될 수 있는 시대, 진짜 개발자의 가치를 찾아서 PDF 다운로드 온라인으로 읽기 새벽 3시, 모니터 앞에서 에너지드링크를 마시며 버그와 씨름하던 그 시절이 있었다. ChatGPT가 내가 3일간 짠 코드를 5분 만에 뚝딱 만들어내는 걸 보고 나서, 문득 이런 생각이 들었다. "개발자들을 위한 나라는 정말 없는 건 아닐까?" 하지만 몇 달 후, 완전히 다른 결론에 도달했다. 이 책은 그 과정의 기록이다. AI 시대에 개발자의 가치가 사라지는 게 아니라, 모든 사람이 개발자가 되는 시대가 오고 있다는 이야기. 목차 프롤로그 개발자라는 직업의 종말과 시작 1장 10년 차 개발자가 ChatGPT에게 밀린 날 2장 AI가 5분 만에 해치운 나의 3일짜리 작업 3장 "코딩 몰라도 앱 만든다"는 거짓말과 진실 4장 카페 사장이 만든 POS 시스템 5장 AI 개발 도구 완전 정복 가이드 6장 코딩을 몰라도 되는 것 vs 반드시 알아야 하는 것 7장 일반인 개발자의 현실적 한계와 극복법 8장 전문 개발자의 생존 전략 9장 모든 직업이 개발자를 포함하는 시대 10장 사이드프로젝트의 종말, 개인프로젝트의 시작 에필로그 개발자로 살아남는다는 것의 새로운 의미 작성 시점 안내 이 책은 2025년 6월의 AI 기술 환경을 기준으로 작성되었습니다. AI의 발전 속도는 상상을 초월하기 때문에, 일부 내용은 현재 시점과 다를 수 있습니다. 당시의 기록으로 읽어주세요. 브라우저가 PDF 뷰어를 지원하지 않습니다. PDF를 다운로드하세요.
이온디
이온디 1개월 전
학습 목표 디자인 토큰의 개념 이해 Figma Dev Mode에서 정확한 CSS 값 추출 SCSS 변수 시스템 구축 디자인-퍼블 불일치 방지 1. 왜 디자인 토큰이 필요한가? Figma MCP의 한계 Figma MCP는 레이아웃 구조를 빠르게 가져오는 데 탁월하지만, 정밀한 수치를 정확히 추출하는 데는 한계가 있습니다. 잘 가져오는 것 못 가져오는 것 레이아웃 구조 (Flexbox, Grid) 정확한 font-size 색상 값 (HEX, RGB)… 학습 목표 디자인 토큰의 개념 이해 Figma Dev Mode에서 정확한 CSS 값 추출 SCSS 변수 시스템 구축 디자인-퍼블 불일치 방지 1. 왜 디자인 토큰이 필요한가? Figma MCP의 한계 Figma MCP는 레이아웃 구조를 빠르게 가져오는 데 탁월하지만, 정밀한 수치를 정확히 추출하는 데는 한계가 있습니다. 잘 가져오는 것 못 가져오는 것 레이아웃 구조 (Flexbox, Grid) 정확한 font-size 색상 값 (HEX, RGB) line-height, letter-spacing 대략적인 크기 margin/padding 정밀 값 요소 순서 border-radius 세부 값 실제 사례: lllayer_kda 프로젝트 프롬프트: 피씨 이 1개의 디자인을 Figma에서 구현하세요. @https://www.figma.com/design/... 결과: - ✅ 구조는 맞음 (섹션 배치, Flexbox) - ❌ 글자 크기가 다름 (48px → 40px로 잘못 해석) - ❌ 간격이 다름 (80px → 60px로 잘못 적용) - ❌ 정렬이 안 맞음 원인: AI가 디자인을 "추론"하여 값을 생성하기 때문 2. 디자인 토큰이란? 디자인 시스템의 가장 작은 단위로, 재사용 가능한 값들의 모음입니다. Typography 토큰 토큰명 설명 예시 값 font-family 폰트 종류 'Pretendard', sans-serif font-size-h1 제목1 크기 64px font-size-h2 제목2 크기 48px font-size-body 본문 크기 16px font-weight-bold 굵은 글씨 700 line-height-normal 기본 행간 1.5 letter-spacing-tight 자간 (좁음) -0.02em Spacing 토큰 토큰명 값 용도 spacing-xs 4px 아이콘-텍스트 간격 spacing-sm 8px 작은 요소 간격 spacing-md 16px 기본 패딩 spacing-lg 24px 섹션 내 간격 spacing-xl 48px 섹션 간 간격 spacing-xxl 80px 대형 섹션 간격 Color 토큰 토큰명 값 용도 color-primary #0066FF 브랜드 메인 color-secondary #6B7280 보조 색상 color-background #FFFFFF 배경 color-surface #F9FAFB 카드 배경 color-text-primary #111827 본문 텍스트 color-text-secondary #6B7280 부제목 텍스트 3. Figma에서 토큰 추출하기 방법 1: Dev Mode 활용 (가장 정확) Figma에서 요소 선택 우측 Inspect 패널 확인 CSS 값 복사 Dev Mode에서 보이는 값: font-size: 64px line-height: 76.8px (= 1.2) letter-spacing: -1.28px (= -0.02em) 방법 2: Figma Variables 활용 디자이너가 Variables로 정의한 경우: # Claude 프롬프트 이 Figma 파일의 디자인 토큰(Variables)을 추출해주세요. @https://www.figma.com/design/... 또는 MCP 도구 직접 호출: get_variable_defs nodeId="xxx" 방법 3: 디자이너에게 스펙 문서 요청 디자이너에게 다음 항목을 요청하세요: [ ] Typography 스펙 (H1~H6, Body, Caption 등) [ ] Spacing 스펙 (4px 배수 권장) [ ] Color 팔레트 [ ] Breakpoint 정의 4. SCSS 변수 시스템 구축 _variables.scss 예시 // ====================== // Typography // ====================== $font-family-primary: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; $font-family-secondary: 'Noto Sans KR', sans-serif; // Font Sizes $font-size-h1: 64px; $font-size-h2: 48px; $font-size-h3: 32px; $font-size-h4: 24px; $font-size-body: 16px; $font-size-caption: 14px; $font-size-small: 12px; // Font Weights $font-weight-light: 300; $font-weight-regular: 400; $font-weight-medium: 500; $font-weight-semibold: 600; $font-weight-bold: 700; // Line Heights $line-height-tight: 1.2; $line-height-snug: 1.3; $line-height-normal: 1.5; $line-height-relaxed: 1.6; $line-height-loose: 1.8; // Letter Spacing $letter-spacing-tight: -0.02em; $letter-spacing-normal: 0; $letter-spacing-wide: 0.05em; // ====================== // Spacing // ====================== $spacing-xs: 4px; $spacing-sm: 8px; $spacing-md: 16px; $spacing-lg: 24px; $spacing-xl: 48px; $spacing-xxl: 80px; $spacing-3xl: 120px; // ====================== // Breakpoints // ====================== $breakpoint-mobile: 360px; $breakpoint-tablet: 768px; $breakpoint-pc: 1920px; $breakpoint-wide: 3440px; // Inner Width (Container) $inner-width-mobile: 324px; $inner-width-tablet: 688px; $inner-width-pc: 1720px; $inner-width-wide: 2590px; // ====================== // Colors // ====================== $color-primary: #0066FF; $color-primary-dark: #0052CC; $color-secondary: #6B7280; $color-background: #FFFFFF; $color-surface: #F9FAFB; $color-border: #E5E7EB; $color-text-primary: #111827; $color-text-secondary: #6B7280; $color-text-muted: #9CA3AF; // ====================== // 반응형 Mixin // ====================== @mixin mobile { @media (max-width: #{$breakpoint-tablet - 1px}) { @content; } } @mixin tablet { @media (min-width: $breakpoint-tablet) and (max-width: #{$breakpoint-pc - 1px}) { @content; } } @mixin pc { @media (min-width: $breakpoint-pc) { @content; } } @mixin wide { @media (min-width: $breakpoint-wide) { @content; } } 변수 사용 예시 // _typography.scss .page-title { font-family: $font-family-primary; font-size: $font-size-h1; font-weight: $font-weight-bold; line-height: $line-height-tight; letter-spacing: $letter-spacing-tight; color: $color-text-primary; @include tablet { font-size: $font-size-h2; } @include mobile { font-size: $font-size-h3; } } .section { padding: $spacing-xxl 0; @include mobile { padding: $spacing-xl 0; } } .container { max-width: $inner-width-pc; margin: 0 auto; padding: 0 $spacing-lg; @include wide { max-width: $inner-width-wide; } @include tablet { max-width: $inner-width-tablet; } @include mobile { max-width: $inner-width-mobile; padding: 0 $spacing-md; } } 5. 워크플로우 개선 Before (기존 방식) 1. Figma URL 제공 ↓ 2. "이 디자인을 구현하세요" ↓ 3. 결과 확인 → 수치가 안 맞음 ↓ 4. "글자 크기 48px로 수정해주세요" ↓ 5. "간격 80px로 수정해주세요" ↓ 6. 반복... (비효율) After (토큰 기반 방식) 1. 프로젝트 시작 시 디자인 토큰 정의 ↓ 2. _variables.scss 파일 생성 ↓ 3. Figma URL + 토큰 명시 프롬프트 ↓ 4. 결과물 일관성 확보 (수정 최소화) 6. 실습: lllayer_kda 토큰 정의 Step 1: Figma에서 값 추출 피그마 Dev Mode에서 확인할 항목: Typography - [ ] Title (H1~H4) 폰트 사이즈 - [ ] Body text 폰트 사이즈 - [ ] Caption 폰트 사이즈 - [ ] Font weight 종류 - [ ] Line height 값 Spacing - [ ] 섹션 간 간격 - [ ] 컨테이너 패딩 - [ ] 요소 간 간격 Layout - [ ] Container max-width (브레이크포인트별) - [ ] Grid gap Step 2: _variables.scss 작성 파일 위치: layouts/lllayer_kda/assets/css/_variables.scss // lllayer_kda 디자인 토큰 // Figma Dev Mode에서 추출한 정확한 값 // Typography $font-size-hero: 120px; // 메인 타이틀 $font-size-h1: 64px; $font-size-h2: 48px; $font-size-h3: 32px; $font-size-h4: 24px; $font-size-body: 18px; $font-size-caption: 14px; // ... (프로젝트에 맞게 커스터마이즈) Step 3: 기존 SCSS에 변수 적용 Before (하드코딩) .hero-title { font-size: 64px; line-height: 1.2; margin-bottom: 48px; } After (변수 사용) .hero-title { font-size: $font-size-h1; line-height: $line-height-tight; margin-bottom: $spacing-xl; } Step 4: 검증 브라우저 개발자 도구 열기 Computed 탭에서 실제 적용된 값 확인 Figma Dev Mode의 값과 1:1 비교 7. 프롬프트 템플릿 (토큰 기반) 기본 템플릿 작업경로: layouts/lllayer_kda/assets/pages/_about_english.html SCSS경로: layouts/lllayer_kda/assets/css/components/_about.scss ## 디자인 토큰 (반드시 준수) ### Typography | 요소 | font-size | line-height | font-weight | |-----|-----------|-------------|-------------| | H1 | 64px | 1.2 | 700 | | H2 | 48px | 1.3 | 600 | | H3 | 32px | 1.4 | 600 | | Body | 18px | 1.6 | 400 | | Caption | 14px | 1.5 | 400 | ### Spacing | 용도 | 값 | |-----|-----| | Section gap | 80px | | Inner gap | 48px | | Container padding | 24px | ### Breakpoints | 디바이스 | 뷰포트 | Inner width | |---------|--------|-------------| | Wide | 3440px | 2590px | | PC | 1920px | 1720px | | Tablet | 768px | 688px | | Mobile | 360px | 324px | 피씨 이 1개의 디자인을 Figma에서 구현하세요. **위 토큰 값을 정확히 적용해주세요.** @https://www.figma.com/design/...?node-id=XXX 상세 템플릿 (복잡한 페이지용) 작업경로: layouts/lllayer_kda/assets/pages/_service.html ## 디자인 토큰 ### Hero Section - 타이틀: 120px / 1.0 / 700 / letter-spacing: -0.03em - 서브타이틀: 24px / 1.5 / 400 - Section padding: 160px 0 ### Content Section - Section title: 48px / 1.3 / 600 - Body text: 18px / 1.8 / 400 - Card gap: 24px - Section gap: 120px ### SCSS 변수 파일 layouts/lllayer_kda/assets/css/_variables.scss 파일의 변수를 사용하세요. 하드코딩된 px 값 대신 $font-size-*, $spacing-* 변수를 사용해주세요. 피씨 이 1개의 디자인을 Figma에서 구현하세요. @https://www.figma.com/design/...?node-id=XXX [PC 스크린샷 첨부] 8. 핵심 포인트 요약 토큰 먼저, 퍼블 나중 프로젝트 시작 시 디자인 토큰부터 정의하세요. 디자이너와 협업 Typography/Spacing 스펙을 요청하세요. 디자이너가 Figma Variables를 사용하면 더 좋습니다. SCSS 변수 필수 하드코딩 금지. 모든 값은 변수로 관리하세요. 프롬프트에 토큰 명시 AI가 정확한 값을 적용하도록 명시하세요. 체크리스트 프로젝트 시작 시: - [ ] 디자이너에게 Typography 스펙 받기 - [ ] Spacing 시스템 정의 (4px 배수 권장) - [ ] Color 팔레트 정리 - [ ] Breakpoint 정의 파일 생성: - [ ] _variables.scss 파일 생성 - [ ] style.scss에 @import '_variables' 추가 작업 시: - [ ] 하드코딩 대신 변수 사용 - [ ] 프롬프트에 토큰 표 명시 검증: - [ ] 브라우저에서 Computed 값 확인 - [ ] Figma Dev Mode와 1:1 비교 - [ ] 반응형 브레이크포인트별 검증 참고 자료 Figma Dev Mode 가이드 Figma Variables 사용법 Design Tokens 커뮤니티 그룹
이온디
이온디 6개월 전
# SVG 이미지 최적화 가이드 SVG 파일 용량을 줄이는 두 가지 방법을 정리합니다. ## 1. npx 명령어로 즉시 압축 (권장) ### 설치 없이 바로 사용 ```bash # 단일 파일 압축 npx svgo input.svg -o output.svg # 폴더 내 모든 SVG 압축 npx svgo -f ./icons -o ./icons-optimized # 원본 파일 덮어쓰기 npx svgo input.svg # 폴더 내 파일 덮어쓰기 npx sv… # SVG 이미지 최적화 가이드 SVG 파일 용량을 줄이는 두 가지 방법을 정리합니다. ## 1. npx 명령어로 즉시 압축 (권장) ### 설치 없이 바로 사용 ```bash # 단일 파일 압축 npx svgo input.svg -o output.svg # 폴더 내 모든 SVG 압축 npx svgo -f ./icons -o ./icons-optimized # 원본 파일 덮어쓰기 npx svgo input.svg # 폴더 내 파일 덮어쓰기 npx svgo -f ./icons ``` ### 자주 사용하는 옵션 ```bash # viewBox 유지하면서 압축 npx svgo input.svg --config='{"plugins":[{"name":"preset-default","params":{"overrides":{"removeViewBox":false}}}]}' # 여러 번 반복 압축 (더 작아짐) npx svgo input.svg --multipass # 압축 결과만 미리보기 (실제 저장 안 함) npx svgo input.svg --dry-run ``` ### 실제 사용 예시 ```bash # 레이아웃 이미지 폴더 압축 npx svgo -f layouts/el_basic2/assets/img --multipass # 특정 SVG 파일 압축 npx svgo layouts/el_api/assets/img/logo.svg -o layouts/el_api/assets/img/logo.min.svg ``` --- ## 2. Webpack으로 빌드 시 자동 압축 ### 필요한 패키지 설치 ```bash npm install --save-dev svgo image-minimizer-webpack-plugin ``` ### webpack.config.js 설정 ```javascript const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); module.exports = { // ... 기타 설정 optimization: { minimize: true, minimizer: [ '...', // 기존 minimizer 유지 (terser 등) new ImageMinimizerPlugin({ minimizer: { implementation: ImageMinimizerPlugin.svgoMinify, options: { encodeOptions: { multipass: true, plugins: [ 'preset-default', { name: 'removeViewBox', active: false // viewBox 유지 (반응형에 필요) }, { name: 'removeDimensions', active: false // width/height 유지 } ] } } } }) ] } }; ``` ### 프로젝트 실제 설정 (webpack.layout.js) ```javascript const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); // optimization 섹션 optimization: { minimize: isProduction, minimizer: [ '...', new ImageMinimizerPlugin({ minimizer: { implementation: ImageMinimizerPlugin.svgoMinify, options: { encodeOptions: { multipass: true, plugins: [ 'preset-default', { name: 'removeViewBox', active: false }, { name: 'removeDimensions', active: false } ] } } } }) ] } ``` ### 빌드 명령어 ```bash # 레이아웃 빌드 (SVG 자동 압축 포함) npm run build:layout # 개발 모드 (압축 안 함) npm run dev:layout ``` --- ## 3. SVGO 주요 플러그인 | 플러그인 | 설명 | 기본값 | |---------|------|--------| | `removeViewBox` | viewBox 속성 제거 | 활성 | | `removeDimensions` | width/height 제거 | 비활성 | | `removeComments` | 주석 제거 | 활성 | | `removeMetadata` | 메타데이터 제거 | 활성 | | `removeEmptyAttrs` | 빈 속성 제거 | 활성 | | `cleanupIds` | ID 정리/축약 | 활성 | | `convertColors` | 색상 축약 (#ffffff → #fff) | 활성 | | `mergePaths` | path 병합 | 활성 | ### 권장 설정 (반응형 유지) ```javascript plugins: [ 'preset-default', { name: 'removeViewBox', active: false }, // viewBox 유지! { name: 'removeDimensions', active: true }, // width/height 제거 { name: 'removeXMLNS', active: false } // xmlns 유지 ] ``` --- ## 4. 압축 효과 비교 | 파일 | 원본 | 압축 후 | 감소율 | |------|------|---------|--------| | logo.svg | 12KB | 4KB | 67% | | icon-set.svg | 45KB | 18KB | 60% | | illustration.svg | 120KB | 48KB | 60% | 일반적으로 **50-70% 용량 감소** 효과가 있습니다. --- ## 5. 주의사항 ### viewBox를 제거하면 안 되는 경우 - CSS로 크기를 조절하는 SVG - 반응형 아이콘 - 인라인 SVG ### 압축하면 안 되는 SVG - 애니메이션이 포함된 SVG (일부 최적화가 깨질 수 있음) - 특정 ID를 참조하는 SVG (cleanupIds가 ID를 변경할 수 있음) ### ID 참조 문제 해결 ```javascript // cleanupIds 비활성화 plugins: [ { name: 'cleanupIds', active: false } ] ``` --- ## 6. 빠른 참조 ```bash # 가장 자주 사용하는 명령어 npx svgo -f ./path/to/svg/folder --multipass # viewBox 유지하면서 폴더 압축 npx svgo -f ./icons --config='{"plugins":[{"name":"preset-default","params":{"overrides":{"removeViewBox":false}}}]}' ```
이온디
이온디 6개월 전
<style> @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); .page { max-width: 800px; margin: 0 auto; padding: 60px 50px; } … <style> @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); .page { max-width: 800px; margin: 0 auto; padding: 60px 50px; } @media print { .page { padding: 40px; } .page-break { page-break-before: always; } } /* Header */ .header { text-align: center; margin-bottom: 50px; padding-bottom: 40px; border-bottom: 3px solid #2563eb; } .logo { width: 120px; margin-bottom: 20px; } .header h1 { font-size: 32px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; } .header .subtitle { font-size: 18px; color: #2563eb; font-weight: 500; } .header .company { margin-top: 20px; font-size: 14px; color: #666; } /* Section */ .section { margin-bottom: 45px; } .section h2 { font-size: 22px; font-weight: 700; color: #1a1a1a; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #e5e7eb; } .section h3 { font-size: 18px; font-weight: 600; color: #374151; margin: 25px 0 15px 0; } .section p { margin-bottom: 15px; color: #374151; } /* Problem List */ .problem-list { list-style: none; padding: 0; } .problem-list li { padding: 12px 0 12px 30px; position: relative; border-bottom: 1px solid #f3f4f6; } .problem-list li:before { content: "⚠️"; position: absolute; left: 0; } /* Comparison Table */ .comparison-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .comparison-table th, .comparison-table td { padding: 15px; text-align: left; border: 1px solid #e5e7eb; } .comparison-table th { background: #f8fafc; font-weight: 600; color: #1a1a1a; } .comparison-table td:last-child { background: #eff6ff; color: #1e40af; font-weight: 500; } /* Highlight Box */ .highlight-box { background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); border-left: 4px solid #2563eb; padding: 25px; margin: 25px 0; border-radius: 0 8px 8px 0; } .highlight-box p { margin: 0; font-size: 16px; color: #1e40af; font-weight: 500; } /* Speed List */ .speed-list { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 20px 0; } .speed-list li { background: #f0fdf4; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #bbf7d0; } .speed-list li strong { color: #15803d; } /* Demo Table */ .demo-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .demo-table th, .demo-table td { padding: 15px; text-align: left; border-bottom: 1px solid #e5e7eb; } .demo-table th { font-weight: 600; width: 30%; } .demo-table a { color: #2563eb; text-decoration: none; } /* Pricing Card */ .pricing-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 25px 0; } .pricing-card { border: 2px solid #e5e7eb; border-radius: 12px; padding: 25px; text-align: center; } .pricing-card.featured { border-color: #2563eb; background: #eff6ff; } .pricing-card h4 { font-size: 16px; color: #374151; margin-bottom: 10px; } .pricing-card .price { font-size: 28px; font-weight: 700; color: #1a1a1a; margin-bottom: 15px; } .pricing-card .price span { font-size: 14px; font-weight: 400; color: #666; } .pricing-card ul { list-style: none; padding: 0; text-align: left; font-size: 14px; } .pricing-card ul li { padding: 8px 0; padding-left: 20px; position: relative; } .pricing-card ul li:before { content: "✓"; position: absolute; left: 0; color: #2563eb; font-weight: bold; } /* Process */ .process-steps { display: flex; justify-content: space-between; margin: 30px 0; position: relative; } .process-steps:before { content: ""; position: absolute; top: 25px; left: 50px; right: 50px; height: 2px; background: #e5e7eb; } .process-step { text-align: center; position: relative; z-index: 1; } .process-step .number { width: 50px; height: 50px; background: #2563eb; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 18px; margin: 0 auto 10px; } .process-step .label { font-size: 13px; color: #374151; } /* CTA Box */ .cta-box { background: #1e40af; color: #fff; padding: 35px; border-radius: 12px; text-align: center; margin: 30px 0; } .cta-box h3 { color: #fff; font-size: 22px; margin-bottom: 15px; } .cta-box p { color: #bfdbfe; margin-bottom: 10px; } /* About */ .about-list { list-style: none; padding: 0; } .about-list li { padding: 10px 0 10px 30px; position: relative; } .about-list li:before { content: "•"; position: absolute; left: 10px; color: #2563eb; font-size: 20px; } /* Contact */ .contact-box { background: #f8fafc; padding: 30px; border-radius: 12px; text-align: center; } .contact-box h3 { margin-bottom: 20px; } .contact-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; text-align: left; max-width: 500px; margin: 0 auto; } .contact-item { display: flex; align-items: center; gap: 10px; } .contact-item .icon { width: 24px; text-align: center; } .contact-item a { color: #2563eb; text-decoration: none; } /* Footer */ .footer { margin-top: 50px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; font-size: 13px; color: #9ca3af; } </style> <div class="page"> <!-- Header --> <div class="header"> <img src="https://eond.com/files/attach/images/441847/2fd362f6524a9e817a49c35d52ff2f9d.svg" alt="EOND Logo" class="logo"> <h1>XE 사이트 현대화 제안서</h1> <div class="subtitle">React 기반 프론트엔드 전환 솔루션</div> <div class="company">이온디(EOND) · XE 전문 개발</div> </div> <!-- Problem Section --> <div class="section"> <h2>문제 인식</h2> <p>XE 기반 사이트를 오래 운영하셨다면 이런 고민이 있으실 겁니다.</p> <ul class="problem-list"> <li>페이지 이동할 때마다 <strong>전체 새로고침</strong>으로 인한 느린 체감 속도</li> <li><strong>모바일 환경</strong>에서의 불편한 사용자 경험</li> <li>"XE는 옛날 기술"이라는 <strong>이미지 문제</strong></li> <li>그렇다고 <strong>데이터 이전</strong>하며 플랫폼 바꾸기엔 부담</li> </ul> </div> <!-- Solution Section --> <div class="section"> <h2>솔루션: XE + React</h2> <p><strong>기존 XE 구조는 그대로, 프론트엔드만 현대화합니다.</strong></p> <table class="comparison-table"> <thead> <tr> <th>구분</th> <th>기존 XE</th> <th>React 전환 후</th> </tr> </thead> <tbody> <tr> <td>페이지 전환</td> <td>매번 새로고침</td> <td><strong>즉시 전환 (SPA)</strong></td> </tr> <tr> <td>체감 속도</td> <td>클릭마다 대기</td> <td><strong>앱처럼 빠름</strong></td> </tr> <tr> <td>모바일 경험</td> <td>기본 반응형</td> <td><strong>네이티브 앱 수준</strong></td> </tr> <tr> <td>기술 이미지</td> <td>레거시 PHP</td> <td><strong>최신 React 스택</strong></td> </tr> </tbody> </table> <h3>왜 가능한가?</h3> <ul class="about-list"> <li>자체 개발한 <strong>XE REST API 모듈</strong> 활용</li> <li>XE 데이터베이스/관리자 기능 <strong>100% 유지</strong></li> <li>프론트엔드만 React로 교체하는 <strong>비파괴적 전환</strong></li> </ul> </div> <!-- Speed Section --> <div class="section"> <h2>체감 속도 차이</h2> <p>React 기반 SPA(Single Page Application)의 특성상:</p> <div class="highlight-box"> <p>초기 로딩 1회 후, 이후 페이지 전환은 거의 즉시 이루어집니다.</p> </div> <ul class="speed-list"> <li>게시판 목록 → 글 상세<br><strong>즉시</strong></li> <li>카테고리 이동<br><strong>즉시</strong></li> <li>검색 결과 표시<br><strong>즉시</strong></li> </ul> <p>기존 XE처럼 매번 서버에서 HTML을 받아오는 방식이 아니라,<br>필요한 데이터만 API로 받아와 화면을 갱신하기 때문입니다.</p> </div> <!-- Demo Section --> <div class="section"> <h2>라이브 데모</h2> <p>실제 React로 전환된 XE 사이트를 직접 체험해보세요.</p> <table class="demo-table"> <tr> <th>메인 사이트</th> <td><a href="https://eond.com">https://eond.com</a></td> </tr> <tr> <th>블로그</th> <td><a href="https://eond.com/blog">https://eond.com/blog</a></td> </tr> <tr> <th>포트폴리오</th> <td><a href="https://eond.com/portfolio">https://eond.com/portfolio</a></td> </tr> </table> <p><strong>직접 클릭해보시면 속도 차이를 체감하실 수 있습니다.</strong></p> </div> <div class="page-break"></div> <!-- Pricing Section --> <div class="section"> <h2>서비스 구성 및 가격</h2> <div class="pricing-grid"> <div class="pricing-card"> <h4>React 스킨 패키지</h4> <div class="price">50만원<span>~</span></div> <ul> <li>React 레이아웃 스킨</li> <li>React 게시판 스킨</li> <li>React 통합검색 스킨</li> <li>설치 가이드 제공</li> </ul> </div> <div class="pricing-card featured"> <h4>전체 사이트 전환</h4> <div class="price">300만원<span>~</span></div> <ul> <li>XE REST API 모듈</li> <li>React 풀 커스터마이징</li> <li>디자인 반영/신규</li> <li>설치 및 세팅 완료</li> <li>유지보수 안내</li> </ul> </div> <div class="pricing-card"> <h4>API 모듈 단독</h4> <div class="price">200만원<span>~</span></div> <ul> <li>XE REST API 모듈</li> <li>API 문서 제공</li> <li>자체 개발팀용</li> </ul> </div> </div> </div> <!-- Process Section --> <div class="section"> <h2>진행 프로세스</h2> <div class="process-steps"> <div class="process-step"> <div class="number">1</div> <div class="label">문의 및 상담</div> </div> <div class="process-step"> <div class="number">2</div> <div class="label">사이트 분석</div> </div> <div class="process-step"> <div class="number">3</div> <div class="label">견적 안내</div> </div> <div class="process-step"> <div class="number">4</div> <div class="label">개발</div> </div> <div class="process-step"> <div class="number">5</div> <div class="label">검수 및 인도</div> </div> </div> </div> <!-- CTA Section --> <div class="cta-box"> <h3>무료 사이트 진단</h3> <p>관심이 있으시다면, 먼저 현재 사이트를 무료로 분석해드립니다.</p> <p>현재 사이트 속도 측정 · React 전환 시 개선 포인트 · 예상 견적 및 일정</p> </div> <!-- About Section --> <div class="section"> <h2>이온디(EOND) 소개</h2> <ul class="about-list"> <li>XE 기반 디지털 제품 <strong>500개 이상</strong> 제작/판매</li> <li>XE 커뮤니티 <strong>15년 이상</strong> 활동</li> <li>React + XE REST API <strong>자체 개발</strong></li> </ul> </div> <!-- Contact Section --> <div class="contact-box"> <h3>연락처</h3> <div class="contact-grid"> <div class="contact-item"> <span class="icon">🌐</span> <a href="https://eond.com">eond.com</a> </div> <div class="contact-item"> <span class="icon">📧</span> <a href="mailto:eond@eond.com">eond@eond.com</a> </div> <div class="contact-item"> <span class="icon">📞</span> <span>0507-1433-0311</span> </div> <div class="contact-item"> <span class="icon">💼</span> <a href="https://eond.com/services">서비스 안내</a> </div> </div> </div> <!-- Footer --> <div class="footer"> © 2024 EOND. All rights reserved. </div> </div>
하민수 7개월 전
날씨가 너무 추워요 가을 어디갔어, 인터넷설치 날씨가 너무 추워요 가을 어디갔어, 인터넷설치
이온디
이온디 1년 전
[code] <div class="search-wrap"> <form cond="$grant->view" action="{getUrl()}" method="get" onsubmit="return procFilter(this, search)" no-error-return-url="true"> <input type="hidden" name="vid" value="{$vid}" /> <input type="hidden" name="mid" value="{$mid}"… [code] <div class="search-wrap"> <form cond="$grant->view" action="{getUrl()}" method="get" onsubmit="return procFilter(this, search)" no-error-return-url="true"> <input type="hidden" name="vid" value="{$vid}" /> <input type="hidden" name="mid" value="{$mid}" /> <input type="hidden" name="category" value="{$category}" /> <select name="search_target"> @foreach($search_option as $key => $val) @switch($val) @case("제목") {{ $val = "페트이름" }} @break @case("내용") {{ $val = "페트설명" }} @break @default {{ $val = "페트이름+페트설명" }} @endswitch @if(in_array($key, ['title_content', 'title', 'content'])) <option value="{$key}" selected="selected"|cond="$search_target==$key">{$val}</option> @endif @endforeach </select> <input type="text" name="search_keyword" value="{escape($search_keyword, false)}" title="{$lang->cmd_search}" placeholder="검색어를 입력하세요" /> <button type="submit" class="hide" onclick="xGetElementById('board_search').submit();return false;">{$lang->cmd_search}</button> @if($last_division) <a href="{getUrl('page',1,'document_srl','','division',$last_division,'last_division','')}" class="btn">{$lang->cmd_search_next}</a> @endif </form> <!--<form action=""> <select name="search_target"> <option loop="$search_option=>$key,$val" value="{$key}" selected="selected"|cond="$search_target==$key">{$val}</option> </select> <input type="text" placeholder="검색어"> <button type="submit">검색</button> </form>--> </div> [css] .search-wrap{ display: flex; align-items: center; justify-content: flex-end; form{ display: flex; gap: 5px; max-width: 400px; width: 100%; select, input, button{ height: 40px; font-size: 14px; border-radius: 5px; border: 1px solid #dedede; padding: 0 5px; box-sizing: border-box; max-width: 135px; } input[type='text']{ flex: auto; max-width: 100%; } button[type='submit']{ background-color: #0c1c2e; border-color: #0c1c2e; color: #fff; width: 80px; font-weight: bold; } } }
한취센 1년 전
자립준비청년 디지털진로지원 취업실전캠프 자립준비청년을 대상으로 진로 설계, 직무 교육, 인턴십 연계를 통한 자립 역량 강화 지원을 목적으로 과학기술정보통신부가 주최, 정보통신산업진흥원이 주관하여 한국디지털융합진흥원, 강릉과학산업진흥원, 한국취업센터가 운영하는 취업 실전 캠프 및 인턴 연계 사업에 참여할 자립준비청년들을 모집하고 있습니다. <모집내용> - 신청기간: ~2024.10.06(일) 20:00까지 - 모집인원: 최대 30명 (단체 신청 가능) - 캠프일정: 2024.10.14(… 자립준비청년 디지털진로지원 취업실전캠프 자립준비청년을 대상으로 진로 설계, 직무 교육, 인턴십 연계를 통한 자립 역량 강화 지원을 목적으로 과학기술정보통신부가 주최, 정보통신산업진흥원이 주관하여 한국디지털융합진흥원, 강릉과학산업진흥원, 한국취업센터가 운영하는 취업 실전 캠프 및 인턴 연계 사업에 참여할 자립준비청년들을 모집하고 있습니다. <모집내용> - 신청기간: ~2024.10.06(일) 20:00까지 - 모집인원: 최대 30명 (단체 신청 가능) - 캠프일정: 2024.10.14(월)~16(수) / 2박 3일 * 숙소 및 식사, 차비 등 100% 전체 무료 / 캠프 시 취업역량강화 교육 및 인턴기업 공유 진행 - 프로그램: 입사지원서 교육, 면접스킬 훈련 및 스피치 교육, 현직자 네트워킹 및 강릉투어 등 - 캠프장소: 강릉 오죽한옥마을 (개별 숙소 제공, 센터 내 담당자 동행 가능 및 무료) <자격요건 및 혜택> 1) 자격요건: 자립준비청년 (1989년생~2005년생) - 예비 자립준비청년: 아동복지시설 또는 위탁 가정 거주자 (고등학생 미포함) - 자립준비청년: 아동복지시설 또는 위탁 가정에서 퇴소한 자 (청소년 쉼터 입퇴소자 포함) 2) 혜택 - 캠프참여 혜택: 전 과정 무료 - 교통지원: 서울역, 잠실역 경유 셔틀버스 운영 / 지방 거주 교육생 교통비 실비 지원 등 - 인턴십 연계 (참가자 대상 월 최대 250만원 2개월 지급 등) <신청하기> - 신청링크: https://www.flagup.kr/page/digital5.php?slide=3 * 사전 신청자가 많은 경우, 조기 종료될 수 있으니 많은 참여바랍니다. <문의하기> - 문의처: 한국취업센터 운영본부 - 메일: info@k-jobc.co.kr - 연락처: 02-6956-5981, 02-6956-5980 - 카카오톡: https://open.kakao.com/o/sHWbvBFd
이온디
이온디 1년 전
파일 : modules/nproduct/skins/jslocalprograms/itemdetail.blade.php 관리자가 직접 상품상세페이지에서 주문 삭제하는 기능입니다. 라이믹스 v2 블레이드 문법으로 작성했고, 공부하시는 분들도 참조용으로 봐주세요. @auth <div class="cancel-box"> @php $member_srl = $logged_info->member_srl; $item_srl = $item_info->item_srl; $query = "SELECT * FRO… 파일 : modules/nproduct/skins/jslocalprograms/itemdetail.blade.php 관리자가 직접 상품상세페이지에서 주문 삭제하는 기능입니다. 라이믹스 v2 블레이드 문법으로 작성했고, 공부하시는 분들도 참조용으로 봐주세요. @auth <div class="cancel-box"> @php $member_srl = $logged_info->member_srl; $item_srl = $item_info->item_srl; $query = "SELECT * FROM `nstore_cart` WHERE `member_srl` = $member_srl AND `item_srl` = $item_srl"; $oDB = DB::getInstance(); $stmt = $oDB->prepare($query); $stmt->execute(); $output = $stmt->fetchAll(PDO::FETCH_ASSOC); @endphp @php $hasItems = false; @endphp @if($output) @foreach($output as $key => $val) @if((int)$val['order_status'] > 0) @php $hasItems = true; break; // 첫 번째로 조건을 만족하는 항목을 찾으면 루프를 종료합니다. @endphp @endif @endforeach @endif @if($hasItems) <dl class="orderlist-box"> <dt> <span>주문번호</span> <span>주문날짜</span> <span>주문수량</span> <span class="hide">주문상태</span> <span style="color:#fff;">취소하기</span> </dt> @foreach($output as $key => $val) @if((int)$val['order_status'] >= 2) <dd> <span>{{ $val['order_srl'] }}</span> <span data="{{ zdate($val['regdate'],'Y/m/d H:i') }}">{{ zdate($val['purdate'],'Y/m/d H:i') }}</span> <span>{$val['quantity']}</span> <span class="hide"> @switch($val['order_status']) @case ('0') 주문삭제 @break @case ('1') 입금대기 @break @case ('2') 입금완료 @break @case ('6') 구매완료 @break @endswitch ({$val['order_status']}) </span> <span> <input type="text" name="refund"> <button class="btn-cancel" onclick="update_status('{{ $val['order_srl'] }}','Z');return false;">신청취소</button> </span> </dd> @endif @endforeach </dl> @endif <script> function update_status(order_srl, stat) { var params = {}; // 빈 객체 생성 params['order_srl'] = order_srl; params['order_status'] = stat; exec_json('nstore.procNstoreUpdateOrderStatus', params, function(ret_obj) { alert(ret_obj['message']); location.reload(); // 페이지 새로고침 }); } </script> </div> @endauth
이온디
이온디 2년 전
https://www.instagram.com/visuallab.kr/?g=5 https://www.instagram.com/visuallab.kr/?g=5" style="height: 103px;">https://www.instagram.com/visuallab.kr/?g=5 https://www.instagram.com/visuallab.kr/?g=5 https://www.instagram.com/visuallab.kr/?g=5" style="height: 103px;">https://www.instagram.com/visuallab.kr/?g=5
?
닥터루시드 2년 전
안녕하세요. 저는 의사입니다. 비대면의료 플랫폼 닥터루시드를 출시합니다. 안녕하세요. 저는 의사입니다. '닥터루시드'비대면의료 플랫폼을 위해 팀원을 구합니다. 비대면의료라는 큰시장을 '개척'하는 리더가 되고,이러한 시장을 '지속적'으로 더욱 크게 '확장'시키고 해당 업계에 '독보적인 보스'가 되는것에 최종목표가 있습니다. 함께 일하며 큰 성과를 이루고, 미래를 함께 만들어 나갈 수 있는 예비팀원님들의 참여를 기다리고 있습니다. 만약 저희 프로젝트에 흥미를 느끼신다면, … 안녕하세요. 저는 의사입니다. 비대면의료 플랫폼 닥터루시드를 출시합니다. 안녕하세요. 저는 의사입니다. '닥터루시드'비대면의료 플랫폼을 위해 팀원을 구합니다. 비대면의료라는 큰시장을 '개척'하는 리더가 되고,이러한 시장을 '지속적'으로 더욱 크게 '확장'시키고 해당 업계에 '독보적인 보스'가 되는것에 최종목표가 있습니다. 함께 일하며 큰 성과를 이루고, 미래를 함께 만들어 나갈 수 있는 예비팀원님들의 참여를 기다리고 있습니다. 만약 저희 프로젝트에 흥미를 느끼신다면, 언제든지 연락 주시기 바랍니다. 함께 일하며 성공을 만들어 나갈 수 있는 기회를 놓치지 마세요. 감사합니다. 1. 프로젝트 비대면의료 서비스(의료자문+비대면진료) 2. 서비스포인트 올인원 비대면의료 서비스! 작업을 진행할땐 치열하지만 출시후에는 참여팀원뿐아니라,모두가 흥미롭게 즐길 수 있는 어플을 만듭니다!) (1)의료진자문 (2)비대면진료 3. 멤버소개 대표1/크로스앱개발자(RN)2/백엔드5/UIUX2/AI2/제휴1/기획1 [자문의사4인/협력의사10인/심리상담사3] {닥터루시드Team 온보딩영상} https://youtu.be/SsESHaYnZk0 우리 팀의 비전과 함께할 팀원을 구합니다. [랜딩페이지(파일럿ver.) 공유] https://shorturl.at/crA56 [공유 노션/'닥터루시드' long-term Vision] https://url.kr/twrvhd 4. 모집인원 -React native(RN)개발자: 리드 경력자 2명 (모집중)☆☆☆,,ios경력 필수,3년경력,RN앱출시경험,피그마 인터랙션 경험 -QC:1명(모집중)☆☆☆ test시나리오 관리. IT앱 QA/QC경험자 우대. 완성된 실프로덕트 전체과정 운영경험자. SW품질관리 및 테스트(기능/성능/호환성 등 검증) 기획서를 통한 테스트케이스, 체크리스트 작성 및 수행 테스트 계획 수립 및 설계검토 테스트 결과 분석 및 보고서작성 서비스 모니터링 및 운영업무 대응 웹,앱서비스 상품 품질 검증 -광고디자이너 리드급:1명 (모집중) ☆☆☆ 앱 탬플릿제작(상담결과지),앱홍보용카드,고객가이드라인,IR자료디자인,설명서 (디자인만 협조) -전략기획 모집:1명(모집중)☆, 기업에서 대규모앱을 초기 기획부터 끝까지 완수해본 경력자. 전략기획자. -uiux모집:1명 (마감) 피그마 경력자/오토레이아웃/컴포넌트/ 앱출시후에도 계속하실분/ 정식팀원 인정 -BE : 1명 ☆☆☆ (모집중 ) 영상화면 개발가능하신분(카카오 보이스톡같은 영상기능/풀스택경력환영), 대표포함 팀원들과 의사소통이 원활하신분 . spring,ERD,기타서버구축,보안 운영 능통환영. -AI/ML개발자: 2명 (모집중 ) ☆☆☆ (추천모델개발,clustering,음성인식,ai대화(NLP.머신러닝,딥러닝,대화시스템설계,DB,클라우드)) -앱내 플로팅버튼내 큐레이터추천기능 &ai기능추가예정 [팀보딩방법] 오픈채팅방지원 -> [1단계]팀장 또는 팀원 구성 2인면접(서류나 대화로 대체할 수 있음)(경우에 따라 다른방식으로 합류될수있음)->합류확정 6. 작업단계 BE개발 및 FE RN개발진행중,서버개설 & 프로덕트매니징&제품시각화 7.회의일정 -팀별로 주작업 진행 -매주 저녁10시 일요일:전체팀회의(매주 일요일 필참회의/ 일정확인후 지원부탁드립니다.)(2주연속 '무응답' 회의 미참여시 자동 팀방출제도 운영) -프로젝트의 진행단계는, 우선 우리나라 비대면의료 플랫폼 1위를 견고히 한 후에, 세계시장 또한 진출할 것입니다. -닥터루시드는 영업이익을 0순위 최우선으로 생각하며 주식회사적인 운영을 추구합니다(의료플랫폼으로서의 선한영향력은 물론 함께 진행됩니다). 우리 프로젝트에 열정을 가지고 참여하고, 성공을 이끌어낼 책임감과 능력을 가진 소중한 팀원을 기다리고 있습니다. 해당 프로젝트를 완성코자 하신분은 아래 링크로 연락주세요. https://open.kakao.com/o/sNtAK81e https://open.kakao.com/o/sNtAK81e https://open.kakao.com/o/sNtAK81e (경력자 유경험자 우대)