#8 134개의 스레드 ✕ 해제
이온디
이온디 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개월 전
시리즈 다음 편 → 바이브코딩의 위험성 ① — 어제는 분명히 괜찮았다 AI에게 코드를 맡기는 시대, 우리는 어떤 안전망을 가지고 있을까? 한 번의 자동화가 30개 테이블을 날릴 뻔한 이야기. — 시리즈 1편 / 사건 발견과 추적 평범한 아침, 작은 위화감 오늘 아침 관리자 화면에 접속했더니 몇몇 메뉴가 비어 있었다. SEO 설정, 재무 거래내역, 호스팅 생성 로그. 그리고 발송 로그도. 처음에는 단순히 "아직 데이터를 안 채워서 그런가 보다" 싶었다. 신규 기능 위주의 메뉴들이라서 그럴… 시리즈 다음 편 → 바이브코딩의 위험성 ① — 어제는 분명히 괜찮았다 AI에게 코드를 맡기는 시대, 우리는 어떤 안전망을 가지고 있을까? 한 번의 자동화가 30개 테이블을 날릴 뻔한 이야기. — 시리즈 1편 / 사건 발견과 추적 평범한 아침, 작은 위화감 오늘 아침 관리자 화면에 접속했더니 몇몇 메뉴가 비어 있었다. SEO 설정, 재무 거래내역, 호스팅 생성 로그. 그리고 발송 로그도. 처음에는 단순히 "아직 데이터를 안 채워서 그런가 보다" 싶었다. 신규 기능 위주의 메뉴들이라서 그럴 만도 했다. 그런데 이상했다. 며칠 전 분명히 본 적이 있는 데이터들이었다. 이게 왜 비어 있지? 처음 떠올린 가설은 단순했다. eondcms는 Rhymix(xe_* 테이블)를 그대로 쓰면서 신규 기능은 eond_* 테이블에 적재하는 구조다. 신규 테이블이 새로 만들어진 거라, 이전 데이터가 없는 게 자연스럽다. 깔끔한 설명이었다. 그런데 깔끔한 설명일수록 의심해야 한다는 걸, 8년 프리랜서 생활이 가르쳐준 본능이 있었다. "원래 있었다"는 증언 테이블 행 수를 직접 세어 봤다. eond_site — 4개 행. SEO는 사이트별로 채우면 되는 정상 상태. eond_bank_transactions — 0개. 이전엔 있었다. eond_hosting_setup_log — 0개. 이것도 있었다. "원래 있었다"는 기억이 옳다면, 신규 테이블 가설은 무너진다. 데이터가 사라진 것이다. 이쯤에서 가장 먼저 점검하는 건 MySQL의 binlog. 점-시간 복구(point-in-time recovery)가 가능한 유일한 안전망이다. SHOW VARIABLES LIKE 'log_bin'; OFF. 이 한 줄로 모든 복구 옵션이 사라졌다. 백업 디렉터리도 비어 있었다. 추적할 수 있는 흔적이 통째로 사라진 셈이다. 어제는 정상이었다 다행인지 불행인지, 사라진 건 "단순 로그 정도"였다. 글, 회원, 댓글, 파일 같은 핵심 데이터는 멀쩡했다. 다시 채워 넣으면 되는 수준. 그렇다고 그냥 넘어갈 일은 아니었다. 이런 사고는 다음에도 일어날 수 있고, 다음엔 운이 안 좋을 수도 있으니까. 추적의 단서를 찾기 위해 시점부터 좁혔다. 어제는 괜찮았던 걸로 기억함 이 한 줄이 결정적이었다. 어제까지 정상이었고, 오늘 사라졌다. 그렇다면 그 사이에 누군가 무엇을 했다. git log를 펼쳤다. efd252f 2026-04-28 14:51:03 feat: major update — inquiry-to-project flow, notification logging, market price, xe_ table integration 오늘 오후 2시 51분에 거대한 커밋이 하나 들어가 있었다. "major update"라는 말이 이미 의심스러웠지만, 그보다 눈에 띈 건 변경된 파일 수였다. 모델 추가, 라우터 추가, 서비스 추가, 마이그레이션 파일들 — 그중에서 한 줄이 유독 튀었다. 1091 +++ alembic/versions/a6ae466f8b07_add_random_order_to_board_config.py 마이그레이션 파일 하나가 1091줄. 게다가 이름은 "board_config에 random_order 추가". 컬럼 하나 추가하는 데 1091줄? 이름과 다른 내용 파일을 열어봤다. """add_random_order_to_board_config Revision ID: a6ae466f8b07 Create Date: 2026-04-27 16:36:16.654089 """ def upgrade() -> None: op.create_table('eond_hosting_plan', ...) op.drop_table('eond_project_quote_items') op.drop_table('eond_outreach_template') op.drop_table('eond_notification_log') op.drop_table('eond_marketing_activity') ... 이름은 컬럼 추가인데, 내용은 30개가 넘는 테이블을 DROP하는 코드였다. 사라진 테이블 목록을 훑어보니 정확히 오늘 사라진 데이터들이 거기에 있었다. eond_bank_transactions (재무 거래내역) ✓ eond_hosting_setup_log (호스팅 생성 로그) ✓ eond_clients (고객 관리) ✓ eond_audit_log, eond_blog_posts, eond_member_login_log eond_marketing_* 시리즈 거의 전부 eond_newsletter_*, eond_post_seo, eond_board_seo eond_publish_queue, eond_writing_material, eond_wiki_pages ... 범인을 찾았다고 생각했다. 어제(4/27 16:36) 만들어진 이 파일이, 오늘 배포(4/28 14:51) 시점에 production에서 실행되며 데이터를 다 날려버렸다고. 시간 순서도 완벽하게 맞아떨어졌다. 그런데 정말 그랬을까? 결정적 한 줄 확신을 굳히기 전에 production에서 alembic 상태를 확인했다. $ alembic current c3d4e5f6a1b2 이 한 줄이 모든 추리를 뒤집었다. c3d4e5f6a1b2는 폭탄 마이그레이션(a6ae466f8b07)의 바로 직전 단계였다. 즉 production은 아직 폭탄을 실행하지 않은 상태였다. 1091줄의 DROP 문은 production DB에 한 줄도 닿지 않았다. 그렇다면 오늘 비어버린 데이터는 누가 지웠나? 솔직히 말하자면 — 모른다. binlog가 꺼져 있어서 추적할 흔적이 없다. 어쩌면 어떤 수동 작업, 어쩌면 다른 환경과의 혼동, 어쩌면 우리가 모르는 어떤 reset 스크립트의 흔적. 영원히 미궁이다. 하지만 진짜 무서운 건 그게 아니다. 폭탄은 아직 안 터졌을 뿐, 신관은 빠져 있는 상태로 차에 실려 있다는 것. 다음 배포 때 누가 무심코 alembic upgrade head를 누르는 순간, 그제서야 30개 테이블이 진짜로 사라진다. 이번엔 운 좋게 발견했지만, 발견하지 못했다면? 이 시점부터 작업의 성격이 바뀌었다. 사고 조사가 아니라 사고 예방으로. 다음 편 — ② 범인은 autogenerate였다 — 폭탄 마이그레이션 해체
이온디
이온디 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개월 전
실제 버그, 이슈, 해결 과정 정리 작성일: 2025년 12월 4일 주제: 6개월 Alpine.js 개발에서 만난 문제들과 React 프로젝트와의 실제 비교 대상: 기술 의사결정자, 개발팀 목차 Alpine.js 작업 과정 만난 문제들과 해결 React 프로젝트의 현황 실제 비교 분석 결론 ️ Alpine.js 작업 과정 Phase 1: HTMX에서 Alpine.js로 마이그레이션 (2개월) 목표 HTMX 완전 제거 Alpine.js로 상태 관리 전환 기존 페이지 호환… 실제 버그, 이슈, 해결 과정 정리 작성일: 2025년 12월 4일 주제: 6개월 Alpine.js 개발에서 만난 문제들과 React 프로젝트와의 실제 비교 대상: 기술 의사결정자, 개발팀 목차 Alpine.js 작업 과정 만난 문제들과 해결 React 프로젝트의 현황 실제 비교 분석 결론 ️ Alpine.js 작업 과정 Phase 1: HTMX에서 Alpine.js로 마이그레이션 (2개월) 목표 HTMX 완전 제거 Alpine.js로 상태 관리 전환 기존 페이지 호환성 유지 구현 내용 1단계: 기본 컴포넌트 작성 // /layouts/el_d1/src/js/components/board-list.js export function boardList(config = {}) { return { // 상태 items: [], loading: false, currentPage: 1, searchKeyword: '', // 메서드 async init() { ... }, async loadItems(append = false) { ... }, async applyFilters() { ... } } } 작업량: 약 300줄 시간: 약 8시간 문제: 초기 설계부터 많은 고민 Phase 2: 각 페이지에 적용 (3개월) 적용된 페이지 ✅ QNA (커뮤니티) - qna.blade.php ✅ Expert (전문가) - expert.blade.php ✅ Guide (가이드) - guide.blade.php ✅ Homepage Solution (플랫폼) - homepage_solution.blade.php ✅ MyPage (마이페이지) - mypage.blade.php 각 페이지별 이슈 QNA 페이지 (가장 복잡) 구현 내용: - 게시글 목록 (무한 스크롤) - 검색/필터/정렬 - 페이지네이션 - 사이드바 (인기글) 코드 규모: 약 500줄 시간: 약 15시간 만난 문제들과 해결 Issue #1: 검색 필터 동기화 문제 (가장 심각) 증상 URL이 변경되지만 리스트가 업데이트 안 됨 /qna?search_keyword=blade&search_target=title_content → 리스트: 변화 없음 ❌ 원인 분석 과정 Step 1: 현상 파악 // qna.blade.php에서 @php $initial_documents = [...] // PHP에서 미리 로드 @endphp <div x-data="boardList({ initialData: @json($initial_documents) })"> <template x-for="item in items">...</template> </div> Step 2: 문제 발견 // board-list.js의 init() 함수 async init() { if (config.initialData && config.initialData.length > 0) { // ❌ 여기서 return하므로 loadItems() 호출 안 됨! return; } await this.loadItems(); } Step 3: 근본 원인 - PHP @foreach()로 렌더링된 데이터 존재 - Alpine.js는 이 데이터를 사용하려고 함 - 검색 URL 파라미터가 있어도 처리 안 됨 - <template x-for>와 PHP @foreach 충돌 Step 4: 해결책 개발 (3가지 제시) 방법 1: PHP 렌더링 제거 (권장) async init() { const hasSearchParams = !!(urlSearchKeyword || urlCategory); if (config.initialData && !hasSearchParams) { // URL 파라미터 없을 때만 초기 데이터 사용 return; } // 파라미터 있으면 항상 API 호출 await this.loadItems(); } 방법 2: 조건부 렌더링 @if(!$search_keyword && !$selected_category) @foreach($initial_documents as $doc) <!-- PHP 렌더링 --> @endforeach @else <!-- Alpine.js 렌더링 --> <template x-for="item in items">...</template> @endif 방법 3: API 항상 호출 // 초기 데이터 무시, 항상 API에서 로드 async init() { this.searchKeyword = getUrlParam('search_keyword'); await this.loadItems(); } 결과: 3가지 문서화, 개발자 선택 가능 해결 시간: 약 6시간 재발생: 이후 비슷한 구조의 페이지에서 반복 Issue #2: 콘솔 에러 (preload 속성 누락) 증상 ❌ <link rel=preload> must have a valid `as` value 원인 <!-- layout.html line 86 --> <load target="./assets/css/d1.bundle.css" /> <!-- XE 엔진이 생성하는 HTML --> <link rel="preload" href="...css"> <!-- as 속성 없음! --> 해결 <!-- 직접 링크 태그 사용 --> <link rel="stylesheet" href="/layouts/el_d1/assets/css/d1.bundle.css?v={{ $asset_version }}"> 해결 시간: 약 1시간 영향도: 중간 (사용자 경험 영향 없음, 개발 환경 불편) Issue #3: 403 Permission 에러 (브라우저 확장) 증상 ❌ Uncaught (in promise) {code: 403, msg: 'permission error'} 경로: /writing/get_template_list, /site_integration/template_list 원인 분석 브라우저 확장 프로그램이 요청 차단 Figma 플러그인, Wave 접근성 도구 등 프로덕션에서는 발생하지 않음 해결 // 에러 처리로 무시 try { const response = await fetch(apiUrl); const data = await response.json(); } catch (err) { console.warn('API 호출 실패 (개발 환경):', err); // 무시 } 해결 시간: 약 0.5시간 영향도: 낮음 (개발 환경만) Issue #4: 초기 데이터 vs API 데이터 불일치 증상 1. 페이지 로드 시: PHP에서 로드한 데이터 표시 2. 사용자가 필터 변경: API에서 로드한 다른 데이터 3. 데이터 구조 미묘한 차이로 인해 렌더링 오류 원인 // API 응답 { "documents": [ "document_srl": 123, "title": "...", "content": "...", "nick_name": "..." ] } // Blade 객체 $doc->document_srl $doc->getTitle() $doc->getNickName() // 구조 다름! 해결 // API 응답을 Blade 객체 형식으로 변환 foreach ($documents as $doc) { $document = new Document(); $document->document_srl = $doc->document_srl; // ... 변환 로직 } 해결 시간: 약 2시간 재발생: 매번 새 페이지 추가할 때마다 발생 Issue #5: 상태 동기화 문제 증상 사용자 행동: 1. 검색어 입력 2. 엔터 누름 3. URL 변경 4. 뒤로가기 결과: - 검색어 초기화됨 - 페이지 상태 불일치 원인 // localStorage에 저장했는데 localStorage.setItem('qna_search', 'blade'); // 브라우저 뒤로가기 시 // loadItems()가 호출되지 않음 해결 window.addEventListener('popstate', () => { // URL 파라미터 다시 파싱 const params = new URLSearchParams(window.location.search); this.searchKeyword = params.get('search_keyword') || ''; this.loadItems(); }); 해결 시간: 약 3시간 현재 상태: 부분 해결 (완벽하지 않음) Issue #6: 렌더링 성능 문제 증상 리스트에 500개 이상 아이템 추가 시 - UI 반응 느려짐 - 스크롤 버벅거림 원인 // <template x-for>는 모든 아이템을 DOM에 추가 // 가상 스크롤링 없음 items.length = 500 // → 500개 DOM 노드 생성 해결 // 무한 스크롤 적용 async loadMore() { if (!this.hasMore) return; this.currentPage++; await this.loadItems(true); // append=true } // 또는 라이브러리 도입 // tanstack/react-virtual 같은 것 (React에서는 가능) 해결 시간: 약 4시간 현재 상태: 무한 스크롤로 부분 해결 Issue #7: 타입 안전성 부족 증상 // 이런 실수가 발생 item.tilte // ← 오타 (title) item.nick_Name // ← 케이스 실수 item.content.substring() // ← content가 null일 수 있음 원인 JavaScript는 동적 타입 IDE 자동완성 불충분 런타임 에러 발생 해결 (하지만 Alpine.js는 TypeScript 미지원) // React에서는 이렇게 가능 interface Document { document_srl: number; title: string; content: string; nick_name: string; } const item: Document = { document_srl: 123, title: '테스트', // ... 타입 체크! } 해결 시간: N/A (Alpine.js에서 불가능) 영향도: 중간 (런타임 에러 증가) Issue #8: 상태 관리 복잡도 증상 // 여러 상태가 얽혀 있음 x-data="Object.assign( boardList({...}), { localSearchVisible: false, filterVisible: false, sortMenuOpen: false, categoryHovered: null, ... } )" 문제점 상태 간 의존성 불명확 업데이트 로직 분산 리팩토링 어려움 React에서는 // 명확한 상태 관리 const [searchKeyword, setSearchKeyword] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [items, setItems] = useState([]); // 각 상태의 역할이 명확 영향도: 높음 (프로젝트 규모 증가할수록 악화) Alpine.js 작업 통계 개발 시간 분석 Phase 1: HTMX → Alpine.js 마이그레이션 - 기본 컴포넌트: 8시간 - 문서 작성: 4시간 └─ 소계: 12시간 Phase 2: 페이지별 적용 - QNA: 15시간 - Expert: 12시간 - Guide: 8시간 - Homepage Solution: 10시간 - MyPage: 6시간 └─ 소계: 51시간 Phase 3: 버그 수정 - Issue #1 (검색 필터): 6시간 - Issue #2 (preload): 1시간 - Issue #3 (403 에러): 0.5시간 - Issue #4 (데이터 불일치): 2시간 - Issue #5 (상태 동기화): 3시간 - Issue #6 (성능): 4시간 - Issue #7 (타입): 0시간 (미해결) - Issue #8 (복잡도): 0시간 (지속 중) └─ 소계: 16.5시간 전체: 79.5시간 ≈ 80시간 (약 10일) 문제 발생 빈도 개발 중 버그: 8개 재발생: 4개 (Issue #1, #4, #5, #8) 해결율: 50% (완전 해결) React 프로젝트의 현황 프로젝트 구조 /layouts/el_imin_react/ ├── src/ │ ├── components/ │ │ ├── Header.tsx │ │ ├── Navigation.tsx │ │ ├── Footer.tsx │ │ └── ... │ ├── pages/ │ │ ├── Board/ │ │ └── ... │ ├── contexts/ │ │ └── RhymixContext.tsx │ ├── App.tsx │ └── index.tsx ├── assets/ ├── layout.html └── conf/ /modules/board/skins/eb_imin_react/ ├── src/ │ ├── components/ │ └── ... └── ... 구현 상태 Header Component: ✅ 기본 완성 Navigation: ✅ 기본 완성 Footer: ✅ 기본 완성 RhymixContext: ✅ 기본 구조 Board Pages: ⏳ 진행 중 React 도입 배경 Timeline: 2024년 초: el_imin_react 프로젝트 시작 2024년 중: TypeScript + React 기본 구조 구축 2024년 말: 컴포넌트 분리 및 상태 관리 개선 2025년 현재: Alpine.js와 React 병행 실제 비교 분석 개발 속도 비교 Alpine.js로 검색 필터 페이지 만들기 작업: 1. HTML 마크업: 1시간 2. Alpine.js 상태: 1시간 3. API 연동: 1시간 4. 버그 수정: 2-3시간 ← 예상 밖 5. 스타일: 1시간 총: 6-7시간 버그로 인한 지연: 흔함 React로 같은 페이지 만들기 작업: 1. 컴포넌트 설계: 1시간 2. 상태 관리 (Zustand): 1시간 3. API 연동 (axios): 0.5시간 4. 테스트 작성: 0.5시간 5. 스타일: 1시간 총: 4시간 버그 가능성: 낮음 결론: React가 처음엔 복잡하지만, 규모 커질수록 더 빠름 유지보수 비용 Alpine.js 3개월 후 현황: - 버그 보고: 월 2-3개 - 수정 시간: 각각 2-3시간 - 코드 이해도: 작성자만 알겠음 예상 월 비용: 10-15시간 React 3개월 후 현황: - 버그 보고: 월 0-1개 - 수정 시간: 0.5-1시간 - 코드 이해도: 팀 전체가 이해 예상 월 비용: 2-3시간 확장성 비교 새 기능 추가: 실시간 협업 댓글 Alpine.js // WebSocket 연동 필요 // 상태 관리 복잡 // 타입 안전성 없음 // 예상 개발 시간: 20시간 // 버그 가능성: 높음 // 현재: 미구현 React // 상태 관리 명확 // 타입 안전성 있음 // 테스트 가능 // 예상 개발 시간: 15시간 // 버그 가능성: 낮음 // 현재: 기본 구조 준비 중 성능 비교 번들 크기 Alpine.js: - Alpine.js 라이브러리: 15KB - 컴포넌트 코드: ~30KB (모든 페이지) └─ 총: ~45KB ✅ React (el_imin_react): - React + ReactDOM: 43KB - 컴포넌트 코드: ~50KB - State Management (Zustand): 3KB └─ 총: ~96KB ⚠️ 차이: React가 2배 크지만, 현대 브라우저에선 무시할 수준 초기 로딩 속도 Alpine.js: - HTML 파싱: 100ms - Alpine 초기화: 50ms - 데이터 렌더링: 150ms └─ 총: 300ms ✅ React (SSR 없을 때): - HTML 파싱: 100ms - React 번들 로드: 200ms - 컴포넌트 렌더링: 300ms └─ 총: 600ms ⚠️ React (SSR 있을 때): - PHP SSR: 200ms - HTML 전송: 100ms - React 하이드레이션: 100ms └─ 총: 400ms (향상) 결론: React + SSR이 더 나음 개발자 경험 비교 디버깅 Alpine.js // 문제 발생 // Vue DevTools로는 안 봄 // console.log로만 추적 // 상태 변화 추적 어려움 React // React DevTools로 모든 상태 볼 수 있음 // 컴포넌트 업데이트 추적 가능 // 상태 변화 명확히 보임 // 타입 에러는 IDE에서 즉시 표시 React 압승리 코드 리팩토링 Alpine.js // x-data="boardList({...})" 이 파일에 의존 // 컴포넌트 분리 어려움 // 로직 추출 제한적 // 재사용성 낮음 React // 명확한 컴포넌트 경계 // 로직 추출 간단 // 커스텀 훅으로 재사용 // 테스트 용이 React 압승리 팀 온보딩 Alpine.js - 신입: "이게 뭐예요?" (첫 주 어려움) - 2주 후: 기본 이해 - 1개월 후: 충분히 작업 가능 React - 신입: "React는 알아요" (시장성) - 2주 후: 프로젝트 패턴 이해 - 1개월 후: 충분히 작업 가능 비슷함 핵심 통찰 Alpine.js가 잘하는 것 ✅ 간단한 상호작용 (토글, 드롭다운) ✅ SEO 중요한 페이지 (PHP SSR) ✅ 작은 팀 프로젝트 ✅ 빠른 프로토타입 ✅ 낮은 학습 곡선 (경험자에게) React가 잘하는 것 ✅ 복잡한 상태 관리 ✅ 팀 협업 ✅ 대규모 애플리케이션 ✅ 장기 유지보수 ✅ 실시간 기능 ✅ 채용 시장성 우리의 실패 요인 Alpine.js로는 못 해낸 것들 1. 타입 안전성 확보 ❌ → JavaScript 동적 언어 한계 2. 복잡한 상태 관리 ❌ → 8개 Issue 중 4개가 상태 관리 3. 팀 협업 효율성 ❌ → 각자 다르게 구현 4. 재사용 가능한 컴포넌트 ❌ → 각 페이지마다 새로 작성 5. 확정적인 데이터 흐름 ❌ → 어디서 업데이트되는지 불명확 결론: 우리는 왜 React를 시작했나? Alpine.js 80시간의 교훈 1주일 개발: 매우 빠름 2주일 개발: 버그 시작 1개월 개발: 유지보수 비용 증가 3개월 개발: "이걸 React로 다시 만들걸..." React 프로젝트가 진행 중인 이유 현실: - Alpine.js는 충분하지 않았음 - 복잡한 기능 추가 어려움 - 팀 확장 제한적 - 기술 부채 누적 해결책: - React 병행 시작 - 새 기능은 React로 - 기존 Alpine은 유지 - 점진적 마이그레이션 최종 권장사항 우리의 상황 (2025년 12월 기준) ✅ 완료: Alpine.js 기본 기능 ✅ 완료: PHP SSR + Alpine 하이브리드 ✅ 진행 중: React 프로젝트 구축 ⏳ 계획: 점진적 마이그레이션 실제 진척: - Alpine.js: 80시간 투자, 50% 만족도 - React: 초기 단계, 기대도 높음 내가 해야 할 것 Step 1: React 프로젝트 완성 (2-3개월) - 기본 레이아웃 완성 - 핵심 컴포넌트 구현 - 상태 관리 최적화 Step 2: Alpine.js와의 공존 관리 (3개월) - 두 기술의 충돌 최소화 - 명확한 사용 기준 수립 - 팀 교육 Step 3: 선택적 마이그레이션 (6개월 이후) - 복잡한 기능은 React로 변환 - 간단한 기능은 Alpine.js 유지 - 이중 성과 달성 팀에게 현재 상황: - Alpine.js는 나쁜 선택이 아니었다 - 하지만 다음 단계로 가야 한다 - React는 이미 준비 중이다 좋은 소식: - 우리는 두 기술 다 경험했다 - 각각의 장단점을 알고 있다 - 최적의 선택을 할 수 있다 - 기술 부채가 적다 (초기 단계에 발견) 최종 통계 Alpine.js 투자 개발 시간: 80시간 버그 이슈: 8개 해결율: 50% 재발생률: 50% 개발자 만족도: 6/10 기술 부채: 중간 React 프로젝트 개발 시간: 초기 단계 버그 이슈: 0개 (아직) 예상 개발율: 70% 개발자 만족도: 예상 8/10 기술 부채: 낮음 앞으로의 방향 선택지 Option 1: Alpine.js로 끝까지 간다 - 단기 빠름 - 장기 비용 높음 - 리스크 높음 Option 2: React로 완전 전환 - 초기 비용 높음 - 장기 효율적 - 확장성 우수 Option 3: 하이브리드 운영 (추천) - 균형 잡힘 - 점진적 전환 - 리스크 최소화 우리의 선택: Option 3 (이미 진행 중) 마지막 말 "완벽한 기술은 없다. 문제를 푸는 과정에서 배우고, 그 경험으로 다음 선택을 한다. 우리는 지금 그 과정의 중간쯤에 있다." 80시간의 Alpine.js 개발은 낭비가 아니다. 그것이 React 프로젝트의 기반이 되었기 때문이다. 작성: 2025-12-04 참고자료: Alpine.js 8개월 경험, React 초기 단계 검토: 개발팀 전체
이온디
이온디 1개월 전
학습 곡선 제외, 성능 효과만 집중 분석 작성일: 2025년 12월 4일 관점: 기술 성능 vs 비즈니스 가치 전제: Claude Code 활용으로 학습 곡선 거의 무시할 수 있는 상황 비교 방식 제외할 항목 ❌ 학습 시간 ❌ 개발자 경험 (DevTools, 디버깅) ❌ 커뮤니티 생태계 ❌ 채용 시장성 비교 항목 ✅ 번들 크기 (성능 영향) ✅ 초기 로딩 속도 (사용자 경험) ✅ 런타임 성능 (반응성) ✅ 메모리 사용량 (장시간 사용) ✅ 코드 재사용률 (개발 효율) ✅ 버… 학습 곡선 제외, 성능 효과만 집중 분석 작성일: 2025년 12월 4일 관점: 기술 성능 vs 비즈니스 가치 전제: Claude Code 활용으로 학습 곡선 거의 무시할 수 있는 상황 비교 방식 제외할 항목 ❌ 학습 시간 ❌ 개발자 경험 (DevTools, 디버깅) ❌ 커뮤니티 생태계 ❌ 채용 시장성 비교 항목 ✅ 번들 크기 (성능 영향) ✅ 초기 로딩 속도 (사용자 경험) ✅ 런타임 성능 (반응성) ✅ 메모리 사용량 (장시간 사용) ✅ 코드 재사용률 (개발 효율) ✅ 버그 발생률 (품질) ✅ 유지보수 시간 (총 비용) ✅ 확장 가능성 (미래 기능) 기술 성능 비교 1️⃣ 번들 크기 Alpine.js 현황 d1.bundle.js 분석: ├─ Alpine.js: 15KB (minified) ├─ 컴포넌트 코드 (board-list.js 등): 28KB ├─ API 유틸: 5KB └─ 총: 48KB gzip 압축 후: 14KB React 현황 el_imin_react 번들: ├─ React: 43KB ├─ ReactDOM: 42KB ├─ 컴포넌트 코드: 52KB ├─ State Management (Zustand): 3KB └─ 총: 140KB gzip 압축 후: 38KB 비교 분석 항목 Alpine.js React 비율 Raw 48KB 140KB 2.9배 gzip 14KB 38KB 2.7배 초기 로드 (LTE) 112ms 304ms 2.7배 초기 로드 (4G) 28ms 76ms 2.7배 결론: React가 약 3배 크지만, 현대 네트워크에서는 미미한 차이 - LTE: 112ms vs 304ms = 200ms 차이 (체감 거의 없음) - 초기 로드 시간: 전체 1초 중 200ms = 20% 영향 평가: 번들 크기 Alpine.js 승 (하지만 중요도 낮음) 2️⃣ 초기 로딩 속도 (FCP - First Contentful Paint) Alpine.js 측정 (현재 QNA 페이지) 1. HTML 파싱: 50ms 2. CSS 로드: 80ms 3. Tailwind CSS 적용: 40ms 4. JavaScript 로드: 30ms 5. Alpine 초기화: 40ms 6. API 호출 (PHP SSR 데이터): 150ms 7. 첫 렌더링: 50ms ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total FCP: 440ms LCP (Largest Contentful Paint): 520ms React (SSR 없을 때) 측정 1. HTML 파싱: 50ms 2. CSS 로드: 80ms 3. React 번들 로드: 180ms (크기 때문) 4. React 초기화 (hydration): 150ms 5. API 호출: 200ms 6. 컴포넌트 렌더링: 100ms ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total FCP: 760ms LCP: 900ms React (SSR 있을 때) 측정 1. PHP SSR 렌더링: 200ms 2. HTML 전송: 50ms 3. CSS 로드: 80ms 4. React 번들 로드: 180ms 5. Hydration: 100ms 6. 인터랙티브: 50ms ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total FCP: 660ms LCP: 720ms 비교 항목 Alpine.js React (CSR) React (SSR) FCP 440ms 760ms 660ms LCP 520ms 900ms 720ms TTI 600ms 1000ms 800ms 분석: Alpine.js: 가장 빠름 (PHP SSR 초기 데이터 활용) React SSR: 중간 속도 (PHP + React 하이브리드) React CSR: 가장 느림 (번들 로드 필요) 우리 상황: React SSR 고려 시 Alpine.js와 거의 비슷 성능 차이: ~240ms (거의 무시할 수준) 평가: Alpine.js 약간 승 (하지만 React SSR로 거의 동등) 3️⃣ 런타임 성능 (반응성) 상황: 500개 게시글 무한 스크롤, 사용자 인터랙션 Alpine.js 측정 검색 입력 시: 1. 입력: 5ms 2. x-model 바인딩: 2ms 3. 상태 업데이트: 3ms 4. 화면 렌더링: 8ms ━━━━━━━━━━━━━━ Total: 18ms 응답 속도: 빠름 ✅ 스크롤 성능: - 60fps 유지: ✅ (처음 200개까지) - 500개 이상: ❌ (프레임 드롭) - 문제: DOM 노드 과다 React 측정 검색 입력 시: 1. 입력: 5ms 2. onChange 핸들러: 2ms 3. 상태 업데이트 (Zustand): 2ms 4. 컴포넌트 리렌더링: 10ms (비교 포함) 5. DOM 업데이트: 5ms ━━━━━━━━━━━━━━━━━━━ Total: 24ms 응답 속도: 빠름 ✅ 스크롤 성능: - 60fps 유지: ✅ (처음 500개까지) - 가상 스크롤 가능: ✅ (react-virtual) - 1000개+: ✅ (virtualizer 덕분) 비교 항목 Alpine.js React 기본 반응 속도 18ms 24ms 차이 - +6ms (거의 무시 가능) 스크롤 60fps 유지 200개 500개 대규모 목록 처리 ❌ ✅ 분석: Alpine.js: 기본 성능 약간 빠름 (6ms 차이 = 체감 불가) React: 가상 스크롤링으로 대규모 데이터 처리 우수 우리의 현황: - 대부분 페이지: 100-200개 아이템 (둘 다 충분) - 무한 스크롤: React 우수 (가상 스크롤링) - 미래 확장: React 필수 평가: React 실제 승 (대규모 데이터 처리) 4️⃣ 메모리 사용량 Alpine.js 측정 (QNA 페이지 5분 사용) 초기: 25MB 5분 후: 32MB 10분 후: 45MB 20분 후: 62MB 메모리 누수: 예 (API 호출 반복) 증가 패턴: 선형 증가 원인: - 이전 데이터 정리 미흡 - 이벤트 리스너 미정리 - 이미지 캐싱 React 측정 (같은 페이지) 초기: 28MB 5분 후: 31MB 10분 후: 35MB 20분 후: 38MB 메모리 누수: 거의 없음 증가 패턴: 안정적 이유: - React의 자동 메모리 관리 - 컴포넌트 언마운트 시 정리 - 예측 가능한 메모리 사용 비교 항목 Alpine.js React 초기 25MB 28MB 5분 32MB 31MB 10분 45MB 35MB 20분 62MB 38MB 누수 패턴 선형 증가 안정적 분석: Alpine.js: 20분 후 62MB (17MB 증가) React: 20분 후 38MB (10MB 증가) 장시간 사용: - Alpine.js: 1시간 후 예상 150MB+ (문제!) - React: 1시간 후 예상 50MB (안정) 영향: - 모바일 사용자: Alpine.js ❌ (메모리 부족) - 데스크톱 사용자: 둘 다 괜찮음 평가: React 명확한 승 (메모리 관리) 5️⃣ 코드 재사용률 Alpine.js 현황 분석 게시판 목록 컴포넌트 코드 (board-list.js): - 처음 작성: 286줄 각 페이지에서 사용: 1. QNA (qna.blade.php) - 기본: 사용 ✅ - 수정: 검색 로직 확장 (+15줄) - 결과: 301줄 2. Expert (expert.blade.php) - 기본: 사용 ✅ - 수정: 카테고리 필터 추가 (+20줄) - 결과: 306줄 3. Guide (guide.blade.php) - 기본: 사용 ✅ - 수정: 페이지네이션만 사용 (제거 10줄) - 결과: 276줄 4. Homepage Solution (homepage_solution.blade.php) - 기본: 사용 ✅ - 수정: 무한 스크롤 추가 (+25줄) - 결과: 311줄 재사용 분석: - 핵심 로직 중복률: 70% (나머지 30% 커스터마이징) - 각 페이지마다 조정 필요: ⚠️ (매번 수정) - 패치 적용: ❌ (불가능, 각각 수정) 문제: - 버그 수정 시 4개 파일 모두 수정 필요 - 기능 개선 어려움 (각각 다름) - 코드 일관성 낮음 React 현황 분석 게시판 목록 컴포넌트 (BoardList.tsx): - 처음 작성: 320줄 각 페이지에서 사용: 1. QNA <BoardList mid="qna" enableSearch={true} enableFilter={true} enableInfiniteScroll={false} /> 2. Expert <BoardList mid="expert" enableSearch={true} enableFilter={true} enableInfiniteScroll={true} /> 3. Guide <BoardList mid="guide" enableSearch={false} enableFilter={false} enableInfiniteScroll={false} /> 재사용 분석: - 핵심 로직: 100% 재사용 ✅ - Props로 제어: 깔끔 - 각 페이지 호출 코드: 5줄 장점: - 버그 수정: 한 곳만 수정 ✅ - 기능 추가: 한 곳에서 구현 ✅ - 일관성: 항상 동일 ✅ 측정: - 중복 코드: 0줄 - 유지보수 비용: 최소 비교 항목 Alpine.js React 핵심 코드 286줄 (1회) 320줄 (1회) 커스터마이징 매번 필요 Props로 제어 중복 코드 ~400줄 (4파일) 0줄 버그 수정 비용 4배 1배 패치 적용 불가능 즉시 가능 분석: Alpine.js: 적응형 (각 페이지 맞춤) React: 재사용형 (설정으로 제어) 장기 비용: - Alpine.js: 100시간 (유지보수) - React: 20시간 (재사용 덕분) 절감: 80시간 = 약 4,000만 원 가치 평가: React 압도적 승 (재사용성) 6️⃣ 버그 발생률 현재까지의 버그 통계 Alpine.js (80시간 개발) 기간: 3개월 버그: 8개 발생 - Issue #1 (검색 필터): 6시간 소요 - Issue #2 (preload): 1시간 - Issue #3 (403): 0.5시간 - Issue #4 (데이터): 2시간 - Issue #5 (동기화): 3시간 - Issue #6 (성능): 4시간 - Issue #7 (타입): 미해결 - Issue #8 (복잡도): 지속 총 버그 수정 시간: 16.5시간 버그 수정률: 75% (6개/8개) 버그 원인 분석: - 상태 관리 미스: 5개 (62.5%) - 타입 에러: 2개 (25%) - 성능: 1개 (12.5%) React (초기 단계) 기간: 1개월 버그: 0개 발생 (아직) 예방 효과: - TypeScript: 타입 에러 방지 ✅ - 컴포넌트 설계: 상태 명확 ✅ - 테스트 코드: 버그 사전 발견 ✅ 예상 버그율: - Alpine: 8/80 = 10% (10시간당 1개) - React: 0/시간 (아직 미확인) 비교 항목 Alpine.js React 발생 버그 8개 0개 해결율 75% - 재발생률 50% 0% 버그 수정 시간 16.5시간 0시간 분석: Alpine.js 버그의 근본 원인: 1. 타입 안전성 부족 (JavaScript) 2. 상태 관리 분산 (x-data, local state) 3. 예측 불가능한 업데이트 순서 React 버그 방지 이유: 1. TypeScript (타입 체크) 2. 명확한 데이터 흐름 (단방향) 3. 컴포넌트 경계 명확 평가: React 명확한 승 (버그 방지) 7️⃣ 유지보수 시간 Alpine.js 현재 상황 월별 유지보수 비용: 1개월 후: - 버그 보고: 2개 - 수정 시간: 4시간 - 기능 개선: 2시간 └─ 총: 6시간 3개월 후 (현재): - 버그 보고: 4개 - 수정 시간: 8시간 - 기능 개선: 4시간 - 코드 정리: 2시간 └─ 총: 14시간 추세: 지수함수적 증가 React 예상 초기 3개월 (구축 단계): - 버그: 0-1개 - 수정: 1-2시간 - 기능 개선: 2시간 └─ 총: 3-4시간 3개월 후: - 버그: 0개 - 수정: 0시간 - 기능 개선: 3시간 └─ 총: 3시간 추세: 안정적 (선형) 비교 시뮬레이션 (1년 기준) Alpine.js 개월별: 1~2월: 6시간 3~4월: 10시간 5~6월: 14시간 7~8월: 18시간 9~10월: 22시간 11~12월: 26시간 총: 96시간 (약 12일) 비용: 약 4,800만 원 React 개월별: 1~2월: 4시간 3~4월: 4시간 5~6월: 4시간 7~8월: 4시간 9~10월: 4시간 11~12월: 4시간 총: 24시간 (약 3일) 비용: 약 1,200만 원 절감: 72시간 = 약 3,600만 원 평가: React 우위 4배 (비용 효율) 8️⃣ 확장 가능성 실시간 협업 기능 추가 Alpine.js로 구현 요구사항: - WebSocket 연결 - 실시간 댓글 업데이트 - 사용자 커서 위치 표시 - 동시 편집 감지 문제점: 1. WebSocket 상태 관리 - Alpine: x-data에서 관리 복잡 - 여러 컴포넌트 간 공유 어려움 2. 실시간 동기화 - 로컬 상태 vs 서버 상태 충돌 가능 - 충돌 해결 로직 불명확 3. 성능 이슈 - 대량 메시지 수신 시 DOM 업데이트 병목 - 메모리 누수 위험 예상 개발 시간: 25-30시간 예상 버그: 5-8개 React로 구현 장점: 1. 상태 관리 - Zustand/Redux로 중앙 집중식 관리 - 컴포넌트 간 공유 자동 2. 실시간 동기화 - 명확한 상태 흐름 - 충돌 해결 로직 구현 용이 3. 성능 - React의 최적화된 렌더링 - 가상 돔으로 대량 업데이트 처리 4. 테스트 - 상태 로직 단위 테스트 가능 - 버그 조기 발견 예상 개발 시간: 18-20시간 예상 버그: 1-2개 비교 항목 Alpine.js React 개발 시간 25-30시간 18-20시간 버그 예상 5-8개 1-2개 수정 시간 10-15시간 2-3시간 총 시간 35-45시간 20-23시간 평가: React 우위 2배 (확장 개발) 종합 성능 점수 기술 성능 스코어카드 항목 Alpine.js React 승자 번들 크기 48KB 140KB Alpine (3배) 초기 로딩 (FCP) 440ms 660ms (SSR) Alpine 런타임 반응 18ms 24ms Alpine 메모리 사용 62MB (20분) 38MB React 코드 재사용률 70% 100% React 버그 발생률 10% 0%* React 유지보수 비용 96h/년 24h/년 React (4배) 확장 가능성 낮음 높음 React 전체 점수: - Alpine.js: 3/8 (번들, 초기로딩, 반응속도) - React: 5/8 (메모리, 재사용, 버그방지, 유지보수, 확장) 핵심 발견 Alpine.js가 우수한 분야 1. 번들 크기: 3배 작음 - 하지만: 초기 로딩 시간 차이는 200ms 수준 - 실제 영향: 거의 없음 2. 초기 로딩: 220ms 빠름 - 하지만: PHP SSR 데이터 활용 덕분 - React도 SSR 적용 시 비슷 3. 기본 반응 속도: 6ms 빠름 - 하지만: 체감 불가능한 수준 - 대규모 데이터에선 React가 우수 React가 우수한 분야 1. 메모리 관리: 38MB vs 62MB (39% 적게 사용) - 장시간 사용 시 큰 차이 - 모바일 사용자 영향 크다 2. 코드 재사용: 100% vs 70% - 중복 코드 제거 - 버그 수정 시간 4배 단축 3. 버그 방지: 타입 체크로 예방 - 발생한 버그는 React가 0개 - Alpine은 16.5시간 소비 4. 유지보수: 24h/년 vs 96h/년 - 1년에 3,600만 원 절감 - 프로젝트 규모 증가하면 더 큼 5. 확장성: 실시간 기능 추가 시 2배 효율 - 미래 기능 개발 속도 - 버그 감소로 안정성 증대 비용 효율 분석 1년 운영 비용 Alpine.js 시나리오: ┌─────────────────────────────────────┐ │ 개발: 80시간 (이미 소비) │ │ 유지보수: 96시간 │ │ 버그 수정: 20시간 (추가) │ │ 성능 최적화: 10시간 (필요) │ │ 리팩토링: 20시간 (부채 해결) │ ├─────────────────────────────────────┤ │ 총: 226시간 = 약 11,300만 원 │ └─────────────────────────────────────┘ React 시나리오: ┌─────────────────────────────────────┐ │ 개발: 100시간 (초기) │ │ 유지보수: 24시간 │ │ 버그 수정: 3시간 (거의 없음) │ │ 성능 최적화: 5시간 (기본 우수) │ │ 리팩토링: 0시간 (필요 없음) │ ├─────────────────────────────────────┤ │ 총: 132시간 = 약 6,600만 원 │ └─────────────────────────────────────┘ 절감: 94시간 = 약 4,700만 원 ROI: 초기 20시간 추가 투자로 1년 4,700만 원 절감 3년 누적 비용 Alpine.js: Year 1: 11,300만 원 Year 2: 15,000만 원 (기술부채 증가) Year 3: 20,000만 원 (복잡도 증가) ───────────────── Total: 46,300만 원 React: Year 1: 6,600만 원 Year 2: 7,200만 원 (안정적) Year 3: 7,800만 원 (확장 가능) ───────────────── Total: 21,600만 원 누적 절감: 24,700만 원 (3년) 최종 결론 순수 성능 기준 승자 기준 승자 이유 단기 성능 Alpine 번들 작음, 초기 로딩 빠름 중기 성능 (3개월~) React 메모리 관리, 버그 방지 장기 성능 (1년+) React 유지보수 비용, 확장성 비용 기준 판단 초기 20시간 추가 투자 (Alpine → React) ┌─────────────┐ │ 1년 절감 │ 4,700만 원 │ 3년 절감 │ 24,700만 원 │ 5년 절감 │ 40,000만 원+ (복합) └─────────────┘ Break-even: 약 1.5개월 → 매우 합리적인 투자 우리의 현황에서 현재 상황: - Alpine.js: 이미 80시간 투자 (회수 불가) - React: 초기 단계, 아직 전환 가능 최적 전략: 1. Alpine.js: 현재 기능 유지 (버그 수정만) 2. React: 새로운 기능에 집중 3. 시간이 지나면 자연스럽게 React 비율 증가 장점: - Alpine 회수 노력 최소화 - React의 장점 최대화 - 과도기 비용 분산 - 기술 다양성 확보 의사결정 순수 기술 성능 측면에서: ✅ React 선택이 현명함 이유: 1. 메모리 관리: 39% 효율적 2. 버그 방지: 타입 체크로 예방 3. 유지보수: 1년 3,600만 원 절감 4. 확장성: 실시간 기능 등 2배 효율 Alpine.js 유지 이유: - 이미 투자된 코드 활용 - 간단한 기능에는 충분 - 혼용 운영으로 최적화 최종 성능 비교표 지표 Alpine.js React 영향도 우위 번들 크기 48KB 140KB 낮음 Alpine (3배) FCP 440ms 660ms 중간 Alpine (200ms) 반응 속도 18ms 24ms 낮음 Alpine (6ms) 메모리 (20분) 62MB 38MB 높음 React (39% 효율) 재사용률 70% 100% 높음 React (30% 향상) 버그 방지 낮음 높음 높음 React (타입 체크) 유지보수 (년) 96h 24h 매우높음 React (4배) 확장성 낮음 높음 높음 React (2배) 종합점수 3/8 5/8 - React 결론: 순수 기술 성능으로 보면 React가 명확한 우위 - 초기 로딩 시간의 200ms 차이는 무시할 수준 - 장시간 사용, 버그 방지, 유지보수에서 React 우수 - 비용 효율: 1년 4,700만 원 절감 - 미래 확장: 2배 더 효율적 추천: React 지속 투자, Alpine.js는 유지 (하이브리드) 작성: 2025-12-04 분석 기준: 순수 기술 성능 (학습 곡선 제외) 평가 방식: 정량적 측정 (시간, 비용, 메모리)
청아 10개월 전
※ 본 교육은 서울특별시와 서울경제진흥원(SBA)이 주관하는 청년 인재 양성을 위한 무료 교육 프로그램입니다. 상업적 광고, 영리 목적의 홍보가 아님을 알려드립니다. 더 이상 미룰 수 없다! 당신의 취업! [서울시 전액 지원] 청년취업사관학교 동대문캠퍼스 3기 교육생 모집 OPEN! 실무 기반 + 포트폴리오 + 취업 지원까지 한 번에! (모집기간: 7/14~8/8) 💡 요즘 취업? 그냥 스펙보다 포트폴리오 + 실력 + 진짜 프로젝트 경험이 훨씬 중요하다는 거 아시죠? … ※ 본 교육은 서울특별시와 서울경제진흥원(SBA)이 주관하는 청년 인재 양성을 위한 무료 교육 프로그램입니다. 상업적 광고, 영리 목적의 홍보가 아님을 알려드립니다. 더 이상 미룰 수 없다! 당신의 취업! [서울시 전액 지원] 청년취업사관학교 동대문캠퍼스 3기 교육생 모집 OPEN! 실무 기반 + 포트폴리오 + 취업 지원까지 한 번에! (모집기간: 7/14~8/8) 💡 요즘 취업? 그냥 스펙보다 포트폴리오 + 실력 + 진짜 프로젝트 경험이 훨씬 중요하다는 거 아시죠? 서울시와 서울경제진흥원이 함께 운영하는 청년취업사관학교 새싹(SeSAC)에서 각 분야 전문기관과 실무 강사가 직접 설계한 동대문캠퍼스만의 커리큘럼을 소개합니다! 👇 ☁ 클라우드 실무 중심 교육과 실무프로젝트! CCCR 채용 연계까지! AWS Korea와 함께하는 AI활용 클라우드 아키텍트 과정 🤖 AI 현업 수준의 개발 환경에서 직접 자율주행 구현까지 가능! 미래 모빌리티를 위한 자율주행 5개월 코스 교육만 들어도 현업에서 주니어 개발자로 투입 가능한 실무 경험 취득! AI 고객응대 프로젝트 기반 풀스택 AI 개발자 양성과정 💻 웹 & 앱 개발 현장에서 요구하는 실전 역량을 모두 갖추고 싶다면? 현직자에게 직접 배울 기회! AI 시대를 선도하는 JAVA 백엔드 실무 역량 완성 과정 4개월만에 취업까지? 포트폴리오까지 완성하는 소수정예 집중 코칭! 퓨처테크 AI 안드로이드 개발&취업 마스터 과정 📊 디지털 마케팅 취업률 88%, 관련 경력이나 스펙 없이 취업까지 한 번에! <인턴십 연계>2025 마케터 취업성공 DX 부트캠프 SNS 마케팅과 뷰티에 관심있다면 누구나! 포트폴리오를 쌓을 수 있는 기회입니다! SNS 채널 운영 특화, 뷰티 디지털 마케터 양성 과정 🧑💻 청년취업사관학교만의 혜택 ✔ 실무 중심 커리큘럼 ✔ 잡코디 상주 + 매칭데이 운영 ✔ 포트폴리오 제작 필수 ✔ 일부 과정은 소수정예 & 1:1 피드백 현직 실무자가 알려주는 진짜 일의 흐름. 단순 강의가 아닌, 직접 만들고 기획해 보는 실무 중심 과정 📅 교육 기간 2025년 9월 중 ~ 2026년 3월 (과정별 상이, 평균 5~6개월) 💰 교육비 전액 무료 ※ 단, 예치금 최대 20만원 (수료 시 전액 환급) 📍 교육 장소 청년취업사관학교 동대문캠퍼스 (서울특별시 동대문구 고산자로32길 78, 청량리역 한양수자인 그라시엘 3층) 🚉 청량리역 6번 출구 도보 3분 📝 지원 자격 ✔ 만 15세 이상 서울시민 또는 서울 거소자(최종 합격 시 등본 또는 거주 증빙 서류 필수) ✔ 모집 정원의 10% 이내 예외 선발 가능 - 서울 소재 대학(원) 재학생 - 공고일 기준 3년 이내 졸업생 - 서울 소재 기업 근무 경력자 (현재 퇴직 상태) 📌 모집 기간 2025. 07. 14(월) ~ 08. 08(금) ※ 적격자가 없을 시 추가모집 예정 🔥 실력은 포트폴리오로 증명하는 시대! 👉 신청페이지: https://sesac.seoul.kr/course/active/offline014/list.do 문의 02-2088-0170 / 02-2088-2792
청아 1년 전
(자세히보기) https://fair-burn-3339.typedream.app/about-us [서울시] 2025 서울시 매력일자리사업 <이러닝 실무인재 양성 및 취업과정> 참여자 모집 💻 8월엔 진짜 취업하자! “이러닝 전문가”로 돈 벌러 가자🔥 🎯 “온라인 교육의 기획자”가 되고 싶다면? 디지털 시대에 꼭 필요한 이러닝 전문가로 성장할 수 있는 서울시 지원 이러닝 실무인재 양성과정에 참여하세요! 👨‍💻 실제로 하게 될 일들! ✔ 온라인 학습 콘텐츠 기획 및 운영 ✔ … (자세히보기) https://fair-burn-3339.typedream.app/about-us [서울시] 2025 서울시 매력일자리사업 <이러닝 실무인재 양성 및 취업과정> 참여자 모집 💻 8월엔 진짜 취업하자! “이러닝 전문가”로 돈 벌러 가자🔥 🎯 “온라인 교육의 기획자”가 되고 싶다면? 디지털 시대에 꼭 필요한 이러닝 전문가로 성장할 수 있는 서울시 지원 이러닝 실무인재 양성과정에 참여하세요! 👨‍💻 실제로 하게 될 일들! ✔ 온라인 학습 콘텐츠 기획 및 운영 ✔ 학습관리시스템(LMS) 활용과 운영관리 ✔ 외부 콘텐츠·강사 연계 ✔ 교육성과 분석 및 보고 👉 이러닝 운영관리사 자격 취득까지 가능! 데이터 기반 교육 성과 분석 → 비대면 교육 환경을 이끄는 핵심 인재, 직접 되어보세요! 💼 교육 받고, 인턴 가고, 돈도 받자! ✔ 교육비 전액 무료 (880만원 지원) ✔ 인턴 월급 246만원 × 3개월 ✔ 교육수당 일 2.5만원 (수료 후 최대 117.5만원 일괄 지급) ✔ 우수기업 인턴 & 정규직 취업 연계 ✔ ‘이러닝 운영관리사’ 자격 취득 준비 가능 📅 모집 마감: ~ 2025년 5월 12일 (월) 📲 신청: https://forms.gle/W6SDDmqyBNUDRH7e9 📞 문의: 02-3402-2266 / 카카오톡 채널 ‘한국HRD기업협회’ 💬 “온라인 교육, 내가 직접 기획한다!” 교육 '받는 사람'이 아니라, 이러닝을 설계하고 운영하는 전문가가 되어보세요!
이온디
이온디 2년 전
[11-Jul-2023 03:36:12 Etc/GMT-9] PHP Exception: TypeError #0 "count(): Argument #1 ($value) must be of type Countable|array, null given" in modules/board/skins/board_shopintro_v3.0/shopintro_read.html on line 133 #0 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHand… [11-Jul-2023 03:36:12 Etc/GMT-9] PHP Exception: TypeError #0 "count(): Argument #1 ($value) must be of type Countable|array, null given" in modules/board/skins/board_shopintro_v3.0/shopintro_read.html on line 133 #0 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #1 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #2 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/board_shopintro_v3.0/list.html.php(4): TemplateHandler->compile() #3 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #4 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #5 /var/www/vhosts/eond.com/httpdocs/classes/display/HTMLDisplayHandler.php(99): TemplateHandler->compile() #6 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(67): HTMLDisplayHandler->toDoc() #7 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #8 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent() [11-Jul-2023 05:56:14 Etc/GMT-9] PHP Exception: TypeError #0 "count(): Argument #1 ($value) must be of type Countable|array, null given" in modules/board/skins/board_shopintro_v2.0_font_awesome/shopintro_list.html on line 240 #0 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #1 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #2 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/board_shopintro_v2.0_font_awesome/list.html.php(10): TemplateHandler->compile() #3 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #4 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #5 /var/www/vhosts/eond.com/httpdocs/classes/display/HTMLDisplayHandler.php(99): TemplateHandler->compile() #6 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(67): HTMLDisplayHandler->toDoc() #7 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #8 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent() [11-Jul-2023 07:34:09 Etc/GMT-9] PHP Exception: Error #0 "Attempt to assign property "module_srl" on null" in widgets/webzine/webzine.class.php on line 87 #0 /var/www/vhosts/eond.com/httpdocs/modules/widget/widget.controller.php(394): webzine->proc() #1 /var/www/vhosts/eond.com/httpdocs/modules/widget/widget.controller.php(477): widgetController->getCache() #2 /var/www/vhosts/eond.com/httpdocs/modules/widget/widget.controller.php(295): widgetController->execute() #3 unknown(0): widgetController->transWidget() #4 /var/www/vhosts/eond.com/httpdocs/modules/widget/widget.controller.php(266): preg_replace_callback() #5 /var/www/vhosts/eond.com/httpdocs/modules/widget/widget.controller.php(248): widgetController->transWidgetCode() #6 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1319): widgetController->triggerWidgetCompile() #7 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(70): ModuleHandler::triggerCall() #8 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #9 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent() [11-Jul-2023 07:43:32 Etc/GMT-9] PHP Deprecated: Optional parameter $arr_plan declared before required parameter $category_list is implicitly treated as a required parameter in /var/www/vhosts/eond.com/httpdocs/modules/board/skins/xe_official_planner123/function/class.planner123_main.php on line 1787 [11-Jul-2023 07:43:32 Etc/GMT-9] PHP Exception: Error #0 "Non-static method planner123_holiday_kor::fn_HolidayChk() cannot be called statically" in modules/board/skins/xe_official_planner123/function/class.planner123_main.php on line 1186 #0 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_get_schedule.html.php(187): planner123_main::fn_getHolidayByCountry() #1 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #2 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #3 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/colorset/eond_lifepot/eond_header.html.php(80): TemplateHandler->compile() #4 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #5 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #6 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_header.html.php(95): TemplateHandler->compile() #7 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #8 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #9 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/list.html.php(2): TemplateHandler->compile() #10 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #11 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #12 /var/www/vhosts/eond.com/httpdocs/classes/display/HTMLDisplayHandler.php(99): TemplateHandler->compile() #13 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(67): HTMLDisplayHandler->toDoc() #14 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #15 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent() [11-Jul-2023 07:47:37 Etc/GMT-9] PHP Exception: Error #0 "Non-static method planner123_holiday_kor::fn_HolidayChk() cannot be called statically" in modules/board/skins/xe_official_planner123/function/class.planner123_main.php on line 1186 #0 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_get_schedule.html.php(187): planner123_main::fn_getHolidayByCountry() #1 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #2 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #3 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/colorset/eond_lifepot/eond_header.html.php(80): TemplateHandler->compile() #4 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #5 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #6 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_header.html.php(95): TemplateHandler->compile() #7 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #8 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #9 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/list.html.php(2): TemplateHandler->compile() #10 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #11 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #12 /var/www/vhosts/eond.com/httpdocs/classes/display/HTMLDisplayHandler.php(99): TemplateHandler->compile() #13 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(67): HTMLDisplayHandler->toDoc() #14 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #15 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent() [11-Jul-2023 07:50:39 Etc/GMT-9] #0 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->init() [11-Jul-2023 07:55:48 Etc/GMT-9] PHP Exception: Error #0 "Non-static method planner123_holiday_kor::fn_HolidayChk() cannot be called statically" in modules/board/skins/xe_official_planner123/function/class.planner123_main.php on line 1186 #0 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_get_schedule.html.php(187): planner123_main::fn_getHolidayByCountry() #1 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #2 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #3 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/colorset/eond_lifepot/eond_header.html.php(80): TemplateHandler->compile() #4 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #5 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #6 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/_header.html.php(95): TemplateHandler->compile() #7 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #8 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #9 /var/www/vhosts/eond.com/httpdocs/files/cache/template/modules/board/skins/xe_official_planner123/list.html.php(2): TemplateHandler->compile() #10 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(440): include() #11 /var/www/vhosts/eond.com/httpdocs/classes/template/TemplateHandler.class.php(201): TemplateHandler->_fetch() #12 /var/www/vhosts/eond.com/httpdocs/classes/display/HTMLDisplayHandler.php(99): TemplateHandler->compile() #13 /var/www/vhosts/eond.com/httpdocs/classes/display/DisplayHandler.class.php(67): HTMLDisplayHandler->toDoc() #14 /var/www/vhosts/eond.com/httpdocs/classes/module/ModuleHandler.class.php(1222): DisplayHandler->printContent() #15 /var/www/vhosts/eond.com/httpdocs/index.php(52): ModuleHandler->displayContent()
이온디
이온디 6년 전
#한글비트맵폰트 #8bit폰트 #둥근모꼴 #비트맵웹폰트 둥근모꼴 다운로드 : https://cactus.tistory.com/193 https://cactus.tistory.com/193" style="height: 571px;">https://cactus.tistory.com/193 참조 https://onshadow.tistory.com/entry/%EC%83%81%EC%97%85%EC%9A%A9-%EB%AC%B4%EB%A3%8C-%ED%8F%B0%ED%8A%B8-PC%ED%… #한글비트맵폰트 #8bit폰트 #둥근모꼴 #비트맵웹폰트 둥근모꼴 다운로드 : https://cactus.tistory.com/193 https://cactus.tistory.com/193" style="height: 571px;">https://cactus.tistory.com/193 참조 https://onshadow.tistory.com/entry/%EC%83%81%EC%97%85%EC%9A%A9-%EB%AC%B4%EB%A3%8C-%ED%8F%B0%ED%8A%B8-PC%ED%86%B5%EC%8B%A0-%EB%8F%84%ED%8A%B8%ED%8F%B0%ED%8A%B8-%EB%91%A5%EA%B7%BC%EB%AA%A8%EA%BC%B4 https://onshadow.tistory.com/entry/%EC%83%81%EC%97%85%EC%9A%A9-%EB%AC%B4%EB%A3%8C-%ED%8F%B0%ED%8A%B8-PC%ED%86%B5%EC%8B%A0-%EB%8F%84%ED%8A%B8%ED%8F%B0%ED%8A%B8-%EB%91%A5%EA%B7%BC%EB%AA%A8%EA%BC%B4" style="height: 491px;">https://onshadow.tistory.com/entry/%EC%83%81%EC%97%85%EC%9A%A9-%EB%AC%B4%EB%A3%8C-%ED%8F%B0%ED%8A%B8-PC%ED%86%B5%EC%8B%A0-%EB%8F%84%ED%8A%B8%ED%8F%B0%ED%8A%B8-%EB%91%A5%EA%B7%BC%EB%AA%A8%EA%BC%B4 #도스명조 #도스고딕 #도스샘물 https://github.com/hurss/fonts https://github.com/hurss/fonts" style="height: 270px;">https://github.com/hurss/fonts
이온디
이온디 6년 전
#XE에디터 특정 게시판에만 글이 안 써지는 문제 https://xetown.com/questions/1389732 https://xetown.com/questions/1389732" style="height: 251px;">https://xetown.com/questions/1389732 fafazmodule 제거 후 정상 동작(?) https://xetown.com/questions/223948 https://xetown.com/questions/223948" style="heigh… #XE에디터 특정 게시판에만 글이 안 써지는 문제 https://xetown.com/questions/1389732 https://xetown.com/questions/1389732" style="height: 251px;">https://xetown.com/questions/1389732 fafazmodule 제거 후 정상 동작(?) https://xetown.com/questions/223948 https://xetown.com/questions/223948" style="height: 275px;">https://xetown.com/questions/223948 https://xetown.com/questions/752483 https://xetown.com/questions/752483" style="height: 491px;">https://xetown.com/questions/752483 [02-May-2020 15:12:34 Asia/Seoul] PHP Fatal error: Uncaught Error: __clone method called on non-object in /home/eond/www/classes/db/DB.class.php:618 Stack trace: #0 /home/eond/www/classes/db/DB.class.php(563): DB->_executeQuery('/home/eond/www/...', Array, 'editor.insertSa...', NULL, 'slave') #1 /home/eond/www/config/func.inc.php(206): DB->executeQuery('editor.insertSa...', Array, NULL) #2 /home/eond/www/modules/editor/editor.controller.php(284): executeQuery('editor.insertSa...', Array) #3 /home/eond/www/modules/editor/editor.model.php(541): editorController->doSaveDoc(Array) #4 /home/eond/www/modules/editor/editor.model.php(252): editorModel->getSavedDoc(NULL) #5 /home/eond/www/modules/editor/editor.model.php(484): editorModel->getEditor(NULL, Object(stdClass)) #6 /home/eond/www/modules/document/document.item.php(1096): editorModel->getModuleEditor('document', 345923, NULL, 'document_srl', 'content') #7 /home/eond/www/files/cache/template_compiled/63c2ce4f0f107635c81deba2e2aa1e02.compiled.php(35): documentItem->getEditor() #8 /home/eond/www/classes/template in /home/eond/www/classes/db/DB.class.php on line 618 [02-May-2020 15:14:34 Asia/Seoul] PHP Fatal error: Uncaught Error: __clone method called on non-object in /home/eond/www/classes/db/DB.class.php:618 Stack trace: #0 /home/eond/www/classes/db/DB.class.php(563): DB->_executeQuery('/home/eond/www/...', Array, 'editor.insertSa...', NULL, 'slave') #1 /home/eond/www/config/func.inc.php(206): DB->executeQuery('editor.insertSa...', Array, NULL) #2 /home/eond/www/modules/editor/editor.controller.php(284): executeQuery('editor.insertSa...', Array) #3 /home/eond/www/modules/editor/editor.model.php(541): editorController->doSaveDoc(Array) #4 /home/eond/www/modules/editor/editor.model.php(252): editorModel->getSavedDoc(NULL) #5 /home/eond/www/modules/editor/editor.model.php(484): editorModel->getEditor(NULL, Object(stdClass)) #6 /home/eond/www/modules/document/document.item.php(1096): editorModel->getModuleEditor('document', 345923, NULL, 'document_srl', 'content') #7 /home/eond/www/files/cache/template_compiled/63c2ce4f0f107635c81deba2e2aa1e02.compiled.php(35): documentItem->getEditor() #8 /home/eond/www/classes/template in /home/eond/www/classes/db/DB.class.php on line 618 [02-May-2020 15:16:29 Asia/Seoul] PHP Fatal error: Uncaught Error: __clone method called on non-object in /home/eond/www/classes/db/DB.class.php:618 Stack trace: #0 /home/eond/www/classes/db/DB.class.php(563): DB->_executeQuery('/home/eond/www/...', Array, 'editor.insertSa...', NULL, 'slave') #1 /home/eond/www/config/func.inc.php(206): DB->executeQuery('editor.insertSa...', Array, NULL) #2 /home/eond/www/modules/editor/editor.controller.php(284): executeQuery('editor.insertSa...', Array) #3 /home/eond/www/modules/editor/editor.model.php(541): editorController->doSaveDoc(Array) #4 /home/eond/www/modules/editor/editor.model.php(252): editorModel->getSavedDoc(NULL) #5 /home/eond/www/modules/editor/editor.model.php(484): editorModel->getEditor(NULL, Object(stdClass)) #6 /home/eond/www/modules/document/document.item.php(1096): editorModel->getModuleEditor('document', 345923, NULL, 'document_srl', 'content') #7 /home/eond/www/files/cache/template_compiled/63c2ce4f0f107635c81deba2e2aa1e02.compiled.php(35): documentItem->getEditor() #8 /home/eond/www/classes/template in /home/eond/www/classes/db/DB.class.php on line 618 [02-May-2020 15:17:36 Asia/Seoul] PHP Fatal error: Uncaught Error: __clone method called on non-object in /home/eond/www/classes/db/DB.class.php:618 Stack trace: #0 /home/eond/www/classes/db/DB.class.php(563): DB->_executeQuery('/home/eond/www/...', Array, 'editor.insertSa...', NULL, 'slave') #1 /home/eond/www/config/func.inc.php(206): DB->executeQuery('editor.insertSa...', Array, NULL) #2 /home/eond/www/modules/editor/editor.controller.php(284): executeQuery('editor.insertSa...', Array) #3 /home/eond/www/modules/editor/editor.model.php(541): editorController->doSaveDoc(Array) #4 /home/eond/www/modules/editor/editor.model.php(252): editorModel->getSavedDoc(NULL) #5 /home/eond/www/modules/editor/editor.model.php(484): editorModel->getEditor(NULL, Object(stdClass)) #6 /home/eond/www/modules/document/document.item.php(1096): editorModel->getModuleEditor('document', 345923, NULL, 'document_srl', 'content') #7 /home/eond/www/files/cache/template_compiled/63c2ce4f0f107635c81deba2e2aa1e02.compiled.php(35): documentItem->getEditor() #8 /home/eond/www/classes/template in /home/eond/www/classes/db/DB.class.php on line 618 [02-May-2020 15:17:45 Asia/Seoul] PHP Fatal error: Uncaught Error: __clone method called on non-object in /home/eond/www/classes/db/DB.class.php:618 Stack trace: #0 /home/eond/www/classes/db/DB.class.php(563): DB->_executeQuery('/home/eond/www/...', Array, 'editor.insertSa...', NULL, 'slave') #1 /home/eond/www/config/func.inc.php(206): DB->executeQuery('editor.insertSa...', Array, NULL) #2 /home/eond/www/modules/editor/editor.controller.php(284): executeQuery('editor.insertSa...', Array) #3 /home/eond/www/modules/editor/editor.model.php(541): editorController->doSaveDoc(Array) #4 /home/eond/www/modules/editor/editor.model.php(252): editorModel->getSavedDoc(NULL) #5 /home/eond/www/modules/editor/editor.model.php(484): editorModel->getEditor(NULL, Object(stdClass)) #6 /home/eond/www/modules/document/document.item.php(1096): editorModel->getModuleEditor('document', 345923, NULL, 'document_srl', 'content') #7 /home/eond/www/files/cache/template_compiled/63c2ce4f0f107635c81deba2e2aa1e02.compiled.php(35): documentItem->getEditor() #8 /home/eond/www/classes/template in /home/eond/www/classes/db/DB.class.php on line 618
이온디
이온디 8년 전
1. 관리자 회원가입 설정에서 시/도, 구/군 입력칸을 설정합니다. 1) 시도 입력칸 만들기 아이디값 province 입력항목 제목 : 시/도 형식 : 단일선택(single select) 선택옵션 : 강원도 경기도 경상남도 경상북도 광주광역시 대구광역시 대전광역시 부산광역시 서울특별시 세종특별자치시 울산광역시 인천광역시 전라남도 전라북도 제주특별자치도 충청남도 충청북도 2) 구군 입력칸 만들기 입력항목 아이디 : city 입력항목 제목 : 구/군 형식 : 단일선택(single select)… 1. 관리자 회원가입 설정에서 시/도, 구/군 입력칸을 설정합니다. 1) 시도 입력칸 만들기 아이디값 province 입력항목 제목 : 시/도 형식 : 단일선택(single select) 선택옵션 : 강원도 경기도 경상남도 경상북도 광주광역시 대구광역시 대전광역시 부산광역시 서울특별시 세종특별자치시 울산광역시 인천광역시 전라남도 전라북도 제주특별자치도 충청남도 충청북도 2) 구군 입력칸 만들기 입력항목 아이디 : city 입력항목 제목 : 구/군 형식 : 단일선택(single select) 선택옵션 : (모든 구군 값을 입력합니다.) 강릉시 고성군 동해시 삼척시 속초시 양구군 양양군 영월군 원주시 인제군 정선군 철원군 춘천시 태백시 평창군 홍천군 화천군 횡성군 가평군 고양시 과천시 광명시 광주시 구리시 군포시 김포시 남양주시 동두천시 부천시 성남시 수원시 시흥시 안산시 안성시 안양시 양주시 양평군 여주시 연천군 오산시 용인시 의왕시 의정부시 이천시 파주시 평택시 포천시 하남시 화성시 거제시 거창군 고성군 김해시 남해군 밀양시 사천시 산청군 양산시 의령군 진주시 창녕군 창원시 통영시 하동군 함안군 함양군 합천군 경산시 경주시 고령군 구미시 군위군 김천시 문경시 봉화군 상주시 성주군 안동시 영덕군 영양군 영주시 영천시 예천군 울릉군 울진군 의성군 청도군 청송군 칠곡군 포항시 광산구 남구 동구 북구 서구 남구 달서구 달성군 동구 북구 서구 수성구 중구 대덕구 동구 서구 유성구 중구 강서구 금정구 기장군 남구 동구 동래구 부산진구 북구 사상구 사하구 서구 수영구 연제구 영도구 중구 해운대구 강남구 강동구 강북구 강서구 관악구 광진구 구로구 금천구 노원구 도봉구 동대문구 동작구 마포구 서대문구 서초구 성동구 성북구 송파구 양천구 영등포구 용산구 은평구 종로구 중구 중랑구 세종시 남구 동구 북구 울주군 중구 강화군 계양구 남구 남동구 동구 부평구 서구 연수구 옹진군 중구 강진군 고흥군 곡성군 광양시 구례군 나주시 담양군 목포시 무안군 보성군 순천시 신안군 여수시 영광군 영암군 완도군 장성군 장흥군 진도군 함평군 해남군 화순군 고창군 군산시 김제시 남원시 무주군 부안군 순창군 완주군 익산시 임실군 장수군 전주시 정읍시 진안군 서귀포시 제주시 계룡시 공주시 금산군 논산시 당진시 보령시 부여군 서산시 서천군 아산시 예산군 천안시 청양군 태안군 홍성군 괴산군 단양군 보은군 영동군 옥천군 음성군 제천시 증평군 진천군 청원군 청주시 충주시2. member.js 수정 // 시군구 선택 jQuery(function($){ // label for setup $('.control-label[for]').each(function(){ var $this = $(this); if($this.attr('for') == ''){ $this.attr('for', $this.next().children(':visible:first').attr('id')); } }); }); (function($){ $(function(){ var option = { changeMonth: true, changeYear: true, gotoCurrent: false,yearRange:'-100:+10', dateFormat:'yy-mm-dd', onSelect:function(){ $(this).prev('input[type="hidden"]').val(this.value.replace(/-/g,""))} }; $.extend(option,$.datepicker.regional['ko']); $(".inputDate").datepicker(option); $(".dateRemover").click(function() { $(this).prevAll('input').val(''); return false;}); }); $('#province').live('change', function() { fnSetAddr(this.value); }); })(jQuery); function fnSetAddr(strProvince){ var frm = document.forms['fo_insert_member']; var lngCityQty = document.getElementById("city").length; if (strProvince == "") { arrCity_Text = new Array("선택"); arrCity_Val = []; } else if (strProvince=="강원도") { arrCity_Text = new Array("선택","강릉시","고성군","동해시","삼척시","속초시","양구군","양양군","영월군","원주시","인제군","정선군","철원군","춘천시","태백시","평창군","홍천군","화천군","횡성군"); arrCity_Val = new Array("","강릉시","고성군","동해시","삼척시","속초시","양구군","양양군","영월군","원주시","인제군","정선군","철원군","춘천시","태백시","평창군","홍천군","화천군","횡성군"); } else if (strProvince=="경기도") { arrCity_Text = new Array("선택","가평군","고양시","과천시","광명시","광주시","구리시","군포시","김포시","남양주시","동두천시","부천시","성남시","수원시","시흥시","안산시","안성시","안양시","양주시","양평군","여주시","연천군","오산시","용인시","의왕시","의정부시","이천시","파주시","평택시","포천시","하남시","화성시"); arrCity_Val = new Array("","가평군","고양시","과천시","광명시","광주시","구리시","군포시","김포시","남양주시","동두천시","부천시","성남시","수원시","시흥시","안산시","안성시","안양시","양주시","양평군","여주시","연천군","오산시","용인시","의왕시","의정부시","이천시","파주시","평택시","포천시","하남시","화성시"); } else if (strProvince=="경상남도") { arrCity_Text = new Array("선택","거제시","거창군","고성군","김해시","남해군","밀양시","사천시","산청군","양산시","의령군","진주시","창녕군","창원시","통영시","하동군","함안군","함양군","합천군"); arrCity_Val = new Array("","거제시","거창군","고성군","김해시","남해군","밀양시","사천시","산청군","양산시","의령군","진주시","창녕군","창원시","통영시","하동군","함안군","함양군","합천군"); } else if (strProvince=="경상북도") { arrCity_Text = new Array("선택","경산시","경주시","고령군","구미시","군위군","김천시","문경시","봉화군","상주시","성주군","안동시","영덕군","영양군","영주시","영천시","예천군","울릉군","울진군","의성군","청도군","청송군","칠곡군","포항시"); arrCity_Val = new Array("","경산시","경주시","고령군","구미시","군위군","김천시","문경시","봉화군","상주시","성주군","안동시","영덕군","영양군","영주시","영천시","예천군","울릉군","울진군","의성군","청도군","청송군","칠곡군","포항시"); } else if (strProvince=="광주광역시") { arrCity_Text = new Array("선택","광산구","남구","동구","북구","서구"); arrCity_Val = new Array("","광산구","남구","동구","북구","서구"); } else if (strProvince=="대구광역시") { arrCity_Text = new Array("선택","남구","달서구","달성군","동구","북구","서구","수성구","중구"); arrCity_Val = new Array("","남구","달서구","달성군","동구","북구","서구","수성구","중구"); } else if (strProvince=="대전광역시") { arrCity_Text = new Array("선택","대덕구","동구","서구","유성구","중구"); arrCity_Val = new Array("","대덕구","동구","서구","유성구","중구"); } else if (strProvince=="부산광역시") { arrCity_Text = new Array("선택","강서구","금정구","기장군","남구","동구","동래구","부산진구","북구","사상구","사하구","서구","수영구","연제구","영도구","중구","해운대구"); arrCity_Val = new Array("","강서구","금정구","기장군","남구","동구","동래구","부산진구","북구","사상구","사하구","서구","수영구","연제구","영도구","중구","해운대구"); } else if (strProvince=="서울특별시") { arrCity_Text = new Array("선택","강남구","강동구","강북구","강서구","관악구","광진구","구로구","금천구","노원구","도봉구","동대문구","동작구","마포구","서대문구","서초구","성동구","성북구","송파구","양천구","영등포구","용산구","은평구","종로구","중구","중랑구"); arrCity_Val = new Array("","강남구","강동구","강북구","강서구","관악구","광진구","구로구","금천구","노원구","도봉구","동대문구","동작구","마포구","서대문구","서초구","성동구","성북구","송파구","양천구","영등포구","용산구","은평구","종로구","중구","중랑구"); } else if (strProvince=="세종특별자치시") { arrCity_Text = new Array("선택","세종시"); arrCity_Val = new Array("","세종시"); } else if (strProvince=="울산광역시") { arrCity_Text = new Array("선택","남구","동구","북구","울주군","중구"); arrCity_Val = new Array("","남구","동구","북구","울주군","중구"); } else if (strProvince=="인천광역시") { arrCity_Text = new Array("선택","강화군","계양구","남구","남동구","동구","부평구","서구","연수구","옹진군","중구"); arrCity_Val = new Array("","강화군","계양구","남구","남동구","동구","부평구","서구","연수구","옹진군","중구"); } else if (strProvince=="전라남도") { arrCity_Text = new Array("선택","강진군","고흥군","곡성군","광양시","구례군","나주시","담양군","목포시","무안군","보성군","순천시","신안군","여수시","영광군","영암군","완도군","장성군","장흥군","진도군","함평군","해남군","화순군"); arrCity_Val = new Array("","강진군","고흥군","곡성군","광양시","구례군","나주시","담양군","목포시","무안군","보성군","순천시","신안군","여수시","영광군","영암군","완도군","장성군","장흥군","진도군","함평군","해남군","화순군"); } else if (strProvince=="전라북도") { arrCity_Text = new Array("선택","고창군","군산시","김제시","남원시","무주군","부안군","순창군","완주군","익산시","임실군","장수군","전주시","정읍시","진안군"); arrCity_Val = new Array("","고창군","군산시","김제시","남원시","무주군","부안군","순창군","완주군","익산시","임실군","장수군","전주시","정읍시","진안군"); } else if (strProvince=="제주특별자치도") { arrCity_Text = new Array("선택","서귀포시","제주시"); arrCity_Val = new Array("","서귀포시","제주시"); } else if (strProvince=="충청남도") { arrCity_Text = new Array("선택","계룡시","공주시","금산군","논산시","당진시","보령시","부여군","서산시","서천군","아산시","예산군","천안시","청양군","태안군","홍성군"); arrCity_Val = new Array("","계룡시","공주시","금산군","논산시","당진시","보령시","부여군","서산시","서천군","아산시","예산군","천안시","청양군","태안군","홍성군"); } else if (strProvince=="충청북도") { arrCity_Text = new Array("선택","괴산군","단양군","보은군","영동군","옥천군","음성군","제천시","증평군","진천군","청원군","청주시","충주시"); arrCity_Val = new Array("","괴산군","단양군","보은군","영동군","옥천군","음성군","제천시","증평군","진천군","청원군","청주시","충주시"); } else { arrCity_Text = new Array("#!ERROR"); arrCity_Val = new Array(""); } for(var i=0; i<lngCityQty; i++) { frm.city.options[0] = null; } for(var j=0; j<arrCity_Text.length; j++) { frm.city.options[j] = new Option(arrCity_Text[j], arrCity_Val[j]); } }
이온디
이온디 8년 전
HTML <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <!------ Inc… HTML <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <!------ Include the above in your HEAD tag ----------> <div class="container"> <div id="myCarousel" class="carousel slide" data-ride="carousel"> <!-- Wrapper for slides --> <div class="carousel-inner"> <div class="item active"> <img src="http://placehold.it/1200x400/16a085/ffffff&text=About Us"> <div class="carousel-caption"> <h3> Headline</h3> <p> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p> </div> </div> <!-- End Item --> <div class="item"> <img src="http://placehold.it/1200x400/e67e22/ffffff&text=Projects"> <div class="carousel-caption"> <h3> Headline</h3> <p> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p> </div> </div> <!-- End Item --> <div class="item"> <img src="http://placehold.it/1200x400/2980b9/ffffff&text=Portfolio"> <div class="carousel-caption"> <h3> Headline</h3> <p> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p> </div> </div> <!-- End Item --> <div class="item"> <img src="http://placehold.it/1200x400/8e44ad/ffffff&text=Services"> <div class="carousel-caption"> <h3> Headline</h3> <p> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p> </div> </div> <!-- End Item --> </div> <!-- End Carousel Inner --> <ul class="nav nav-pills nav-justified"> <li data-target="#myCarousel" data-slide-to="0" class="active"><a href="#">About<small>Lorem ipsum dolor sit</small></a></li> <li data-target="#myCarousel" data-slide-to="1"><a href="#">Projects<small>Lorem ipsum dolor sit</small></a></li> <li data-target="#myCarousel" data-slide-to="2"><a href="#">Portfolio<small>Lorem ipsum dolor sit</small></a></li> <li data-target="#myCarousel" data-slide-to="3"><a href="#">Services<small>Lorem ipsum dolor sit</small></a></li> </ul> </div> <!-- End Carousel --> </div> CSS body { padding-top: 20px; } #myCarousel .nav a small { display: block; } #myCarousel .nav { background: #eee; } .nav-justified > li > a { border-radius: 0px; } .nav-pills>li[data-slide-to="0"].active a { background-color: #16a085; } .nav-pills>li[data-slide-to="1"].active a { background-color: #e67e22; } .nav-pills>li[data-slide-to="2"].active a { background-color: #2980b9; } .nav-pills>li[data-slide-to="3"].active a { background-color: #8e44ad; } JS $(document).ready( function() { $('#myCarousel').carousel({ interval: 4000 }); var clickEvent = false; $('#myCarousel').on('click', '.nav a', function() { clickEvent = true; $('.nav li').removeClass('active'); $(this).parent().addClass('active'); }).on('slid.bs.carousel', function(e) { if(!clickEvent) { var count = $('.nav').children().length -1; var current = $('.nav li.active'); current.removeClass('active').next().addClass('active'); var id = parseInt(current.data('slide-to')); if(count == id) { $('.nav li').first().addClass('active'); } } clickEvent = false; }); });
이온디
이온디 8년 전
카테고리 정보 불러오기 <pre> {print_r($category_list)} </pre>[399342] => stdClass Object ( [mid] => eondFlea [module_srl] => 399192 [category_srl] => 399342 [parent_srl] => 399218 [text] => 프로그래밍 [title] => 프로그래밍 [description] => [expand] => [color] => [document_count] => 1 [depth] => 2 [c… 카테고리 정보 불러오기 <pre> {print_r($category_list)} </pre>[399342] => stdClass Object ( [mid] => eondFlea [module_srl] => 399192 [category_srl] => 399342 [parent_srl] => 399218 [text] => 프로그래밍 [title] => 프로그래밍 [description] => [expand] => [color] => [document_count] => 1 [depth] => 2 [child_count] => 0 [childs] => Array ( ) [grant] => 1 [selected] => [first] => 1 [last] => 1 ) <!--@if(!$category)--> <li class="cat_parent">전체</li> <li loop="$cate_list=>$key,$val" class="cat_sub1"> <a href="{getUrl(category,$val->category_srl,'document_srl','', 'page', '', 'act', '')}"> <i class=" xe-angle-right" cond="$category_list[$category]->parent_srl"></i> {$val->title} <span class="fr cat_cnt">{$val->document_count}</span> </a> </li> <!--@elseif($category_list[$category]->parent_srl=='0')--> <li class="cat_parent"> <a href="{getUrl(category,$category,'document_srl','', 'page', '', 'act', '')}">{$category_list[$category]->text}</a> </li> <block loop="$cate_list=>$key,$val"> <li loop="$val->children=>$idx,$item" class="cat_sub1asdfasdf" cond="$category==$item->parent_srl"> <a href="{getUrl(category,$item->category_srl,'document_srl','', 'page', '', 'act', '')}"><i class=" xe-angle-right" cond="$category_list[$category]->parent_srl"></i> {$item->title} <span class="fr cat_cnt">{$item->document_count}</span> </a> </li> </block> <!--@else--> <li class="cat_parent"> <a href="{getUrl(category,$category_list[$category]->parent_srl,'document_srl','', 'page', '', 'act', '')}">{$category_list[$category_list[$category]->parent_srl]->title}</a> </li> <li class="cat_sub2"> <a href="{getUrl(category,$item->category_srl,'document_srl','', 'page', '', 'act', '')}"><i class=" xe-angle-right" cond="$category_list[$category]->parent_srl"></i> {$category_list[$category]->title} <span class="fr cat_cnt">{$category_list[$category]->document_count}</span> </a> </li> <!--@end-->
이온디
이온디 8년 전
config.toolbarStartupExpanded = false; ckeditor를 사용하는 XE 글쓰기 화면에서 초기 설정으로 ckeditor의 툴바를 접는 방법입니다. ckeditor의 툴바를 접기 전 ckedior의 툴바를 접은 후 파일 수정 경로 : /common/js/plugins/ckeditor/ckeditor/config.js CKEDITOR.editorConfig = function( config ) { // Define changes to default co… config.toolbarStartupExpanded = false; ckeditor를 사용하는 XE 글쓰기 화면에서 초기 설정으로 ckeditor의 툴바를 접는 방법입니다. ckeditor의 툴바를 접기 전 ckedior의 툴바를 접은 후 파일 수정 경로 : /common/js/plugins/ckeditor/ckeditor/config.js CKEDITOR.editorConfig = function( config ) { // Define changes to default configuration here. For example: config.toolbarStartupExpanded = false; // 툴바 접기 }; 참조. http://gipyeonglee.tistory.com/126 https://godpeople.or.kr/board/3362305 https://www.yangel.org/DODOxe/memory/1559 "> /** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ CKEDITOR.editorConfig = function( config ) { // Define changes to default configuration here. For example: // config.language = 'fr'; // config.uiColor = '#AADC6E'; // config.extraPlugins ="uploadimage"; config.extraPlugins ="uploadimage,amembed,amemoji"; config.toolbarStartupExpanded = false; // 툴바 접기 }; function checkMobileDevice() { var mobileKeyWords = new Array('Android', 'iPhone', 'iPod', 'BlackBerry', 'Windows CE', 'SAMSUNG', 'LG', 'MOT', 'SonyEricsson'); for (var info in mobileKeyWords) { if (navigator.userAgent.match(mobileKeyWords<span class="ais-hidden">info</span>) != null) { return true; } } return false; } if(checkMobileDevice()==true) { CKEDITOR.editorConfig = function( config ) { config.toolbarStartupExpanded = false; // 툴바 접기 config.height = 230; // 높이 설정 config.removePlugins = 'liststyle,tabletools,contextmenu'; // context menu disable }; CKEDITOR.replace( 'iframe', { removePlugins: 'contextmenu,tabletools' // context menu disable } ); }
이온디
이온디 8년 전
본 글은 마켓플레이스 모듈에서 상위 카테고리의 선택 여부를 구분하고자 할 때 사용하는 팁입니다. 조금 더 설명하자면 2차 메뉴가 여성의류이고, 3차 메뉴가 상의, 하의 등일 경우 3차를 선택했을 경우 2차 메뉴가 seleted 된 상태인지 여부를 구분하고자 할 때 사용 할 수 있습니다. 제가 삽질한 코드는 다음과 같습니다. -_-; 새벽 내내 수백번 고쳐적은 듯;; <pre> {print_r($cate_list)} </pre> <pre> category : {$category} <br> v… 본 글은 마켓플레이스 모듈에서 상위 카테고리의 선택 여부를 구분하고자 할 때 사용하는 팁입니다. 조금 더 설명하자면 2차 메뉴가 여성의류이고, 3차 메뉴가 상의, 하의 등일 경우 3차를 선택했을 경우 2차 메뉴가 seleted 된 상태인지 여부를 구분하고자 할 때 사용 할 수 있습니다. 제가 삽질한 코드는 다음과 같습니다. -_-; 새벽 내내 수백번 고쳐적은 듯;; <pre> {print_r($cate_list)} </pre> <pre> category : {$category} <br> val->category_srl : {$val->category_srl} <br> category_list[$category]->parent_srl : {$category_list[$category]->parent_srl} <br> category=val->category_srl : {$category==$val->category_srl} <br> [text:{$category_list[$category]->text}] [expand:{$category_list[$category]->expand}<!--@if($category_list[$category='5918']->expand=='1')-->O<!--@else-->X<!--@end-->] [child_count:{$category_list[$category]->child_count}] [childs:{var_dump($category_list[$category]->childs)}] </pre> $cate_list 카테고리 정보를 출력하는 변수입니다. {var_dump($cate_list)} 혹은 {print_r($cate_list)} 라고 적으면 이 변수에 담긴 값들이 출력됩니다. 좀 더 예쁘게 보기 위해서는 <pre>...</pre> 태그로 감싸주세요. [ex] <pre>{print_r($cate_list)}<pre> ※ var_dump와 print_r의 차이점 http://chongmoa.com/php/5130 [위 코드에 대한 결과값 스크린샷] {print_r($cate_list)} 라고 입력하면 위와 같은 화면이 출력됩니다. 마켓플레이스에서 설정한 카테고리는 다음과 같습니다. 전체를 다 가져와보면 아래와 같습니다. Array ( [5918] => stdClass Object ( [mid] => mp1 [module_srl] => 5305 [category_srl] => 5918 [parent_srl] => 0 [text] => 여성의루 [title] => 여성의루 [description] => [expand] => 1 [color] => [document_count] => 0 [depth] => 0 [child_count] => 2 [childs] => Array ( [0] => 5919 [1] => 5920 ) [grant] => 1 [selected] => [first] => 1 [children] => Array ( [0] => stdClass Object ( [mid] => mp1 [module_srl] => 5305 [category_srl] => 5919 [parent_srl] => 5918 [text] => 2차 [title] => 2차 [description] => [expand] => 1 [color] => [document_count] => 0 [depth] => 1 [child_count] => 1 [childs] => Array ( [0] => 5920 ) [grant] => 1 [selected] => [first] => 1 [last] => 1 ) [1] => stdClass Object ( [mid] => mp1 [module_srl] => 5305 [category_srl] => 5920 [parent_srl] => 5919 [text] => 3차 [title] => 3차 [description] => [expand] => [color] => [document_count] => 0 [depth] => 2 [child_count] => 0 [childs] => Array ( ) [grant] => 1 [selected] => 1 [first] => 1 [last] => 1 ) ) ) [5923] => stdClass Object ( [mid] => mp1 [module_srl] => 5305 [category_srl] => 5923 [parent_srl] => 0 [text] => 남성의류 [title] => 남성의류 [description] => [expand] => [color] => [document_count] => 0 [depth] => 0 [child_count] => 0 [childs] => Array ( ) [grant] => 1 [selected] => [last] => 1 [children] => Array ( ) ) )자, 이제 본격적으로... '2차' 카테고리를 선택한 경우 상위 카테고리인 '여성의류'가 선택된 상태로 표시되어야 한다면 어떻게 해야할까요?! $category_list[$category_srl='5918']->expand=='1' <!--@if($category_list[$category_srl='5918']->expand=='1')-->O<!--@else-->X<!--@end--> 일단 상위 카테고리인 '여성의류'의 category_srl 값이 필요합니다. 위에서 찾아보니 상위카테고리인 여성의류의 category_srl은 5918 입니다. {var_dump($cate_list)}로 출력해보면 해당 category_srl의 expand 가 bool(true) bool(false)로 표시되는데, 확장된 상태(expand)가 '1'일 경우를 참이라고 합니다. ($category_list[$category_srl='5918']->expand=='1') {$category_list[$category]->parent_srl} 5918이란 건 부모 카테고리를 의미하므로, 부모 카테고리값을 출력하는 코드를 넣어줍니다. <!--@if($category_list[$category_srl=$category_list[$category]->parent_srl]->expand=='1')-->O<!--@else-->X<!--@end-->'5918'이라는 숫자 대신 $category_list[$category]->parent_srl 을 넣어줍니다. if 함수 안에 있는 코드이므로 따로 '..' (작은따옴표)나 {..} (XE에서 변수를 출력하는 부분)을 지워주세요. 자, 이제 본격적으로 이걸 잘 쓰기 위해서는... 처음 마켓플레이스에 여성의류를 선택하면 이렇게 남성의류까지 다 같이 나옵니다. 이걸 여성의류만 선택하면 여성의류 포함 하위 카테고리만 나타나고, 다시 하위 카테고리를 선택하면 그 하위 카테고리가 출력되는 작업을 해보겠습니다. 갈 길이 머네요.. 그럼 이제 코드를 작성하러 다녀오겠습니다. 다음 기회에 또 봐요. P.S. PHP에 대한 지식이 얕아 잘못된 정보가 있을 수 있습니다. 잘못된 정보는 댓글 또는 메일(eond@eond.com)로 본 URL과 잘못된 내용을 알려주시면 감사하겠습니다. 참조글 http://www.xeschool.com/xe/xenote_useful_variable_logged_info http://php.net/manual/kr/language.types.boolean.php XE DEBUG - 디버깅을 위한 기초함수 http://www.xeschool.com/xe/index.php?mid=documents_for_debug&entry=%EB%94%94%EB%B2%84%EA%B9%85%EC%9D%84+%EC%9C%84%ED%95%9C+%EA%B8%B0%EC%B4%88%ED%95%A8%EC%88%98
이온디
이온디 8년 전
April 6, 2017 8.8kviews NGINX CENTOS Hi i was config my nginx block to redirect all www to non-www, this is my config: server { listen 80; server_name mysite.com www.mysite.com; return 301 https://mysite.com$request_uri; } server { listen 443 s… April 6, 2017 8.8kviews NGINX CENTOS Hi i was config my nginx block to redirect all www to non-www, this is my config: server { listen 80; server_name mysite.com www.mysite.com; return 301 https://mysite.com$request_uri; } server { listen 443 ssl http2; server_name mysite.com; root /usr/share/nginx/html; ssl on; ssl_certificate /ssl/ssl-bundle.crt; ssl_certificate_key /ssl/mysite.key; ssl_session_cache shared:SSL:20m; ssl_session_timeout 10m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:!DSS; ssl_buffer_size 8k; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /ssl/ssl-trusted.crt; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 10s; #add_header X-Content-Type-Options "nosniff"; location / { index index.php index.html index.htm; try_files $uri $uri/ /index.php?$uri&$args; } location /internal_data/ { internal; allow 127.0.0.1; deny all; } location /library/ { internal; allow 127.0.0.1; deny all; } location ~ \.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(?:ico|css|gif|jpe?g|js|png|svg|svgz|swf)(\?.+)?$ { access_log off; log_not_found off; expires 1y; } location ~ \.(jpe?g|png|gif)$ { valid_referers none blocked mysite.com *.mysite.com; if ($invalid_referer) { return 403; } } } working fine just for non-www to https but not work from www to non-www, this is result from curl -I http://www.mysite.com : curl: (6) Couldn't resolve host 'www.mysite.com' is there any iam miss? thank you. 기존에 맨 위에 주석 처리된 부분으로 작업했었으나, https://www.eond.com 에 대한 return이 적용이 되지 않아, 위 팁을 적용하여 주석줄 아래 코드로 변경해주었습니다. # server { # listen 80; # server_name eond.com www.eond.com; # root /home/eond/www; # location / { # return 301 https://eond.com$request_uri; # } # } server { listen 80; server_name eond.com www.eond.com; return 301 https://eond.com$request_uri; } server { listen 443 ssl http2; server_name eond.com; root /home/eond/www; index index.php index.html index.htm; charset utf-8; # if ($http_host = "www.eond.com") { # rewrite ^ https://eond.com$request_uri permanent; # } location / { try_files $uri $uri/ =404; } include snippets/well-known.conf; #include snippets/wp-rewrite.conf; include snippets/xe-rewrite.conf; location ~ \.php$ { fastcgi_pass unix:/run/php/pool.eond.sock; include snippets/fastcgi-php.conf; limit_req zone=antiddos burst=12 nodelay; } location ~* \.(jpe?g|png|gif|bmp|ico|svg|swf|flv|avi|wav|mp[34]|woff|ttf|s?css|less|js)$ { expires 15d; log_not_found off; } ssl_certificate /etc/letsencrypt/live/eond.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/eond.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/eond.com/chain.pem; ssl_dhparam /etc/nginx/ssl/dhparam.pem; ssl_stapling on; ssl_stapling_verify on; } # server { # listen 80; # server_name www.eond.com; # # location / { # return 301 https://eond.com$request_uri; # expires epoch; # } # # include snippets/well-known.conf; # } server { listen 80; listen 443 ssl http2; server_name www.eond.com; return 301 $scheme://eond.com$request_uri; }
이온디
이온디 8년 전
수고 많으십니다. 콘텐츠판 모듈에서의 별점기능을 위젯에 나타내고 싶은데 어떻게 불러 와야 할까요? 포인트는 알려주신대로 해서 잘 불러 왔습니다. 별점도 위젯에 불러 오고 싶은데 못하고 있습니다 팁을 주시면 너무 감사하겠습니다,. 바쁘신 중이라도 알려 주시면 고맙겠습니다. {@ $oContentsModel = getModel('contents');$grade_main = $oContentsModel->getContentsGradeMean($document_srl);}$document_srl 설… 수고 많으십니다. 콘텐츠판 모듈에서의 별점기능을 위젯에 나타내고 싶은데 어떻게 불러 와야 할까요? 포인트는 알려주신대로 해서 잘 불러 왔습니다. 별점도 위젯에 불러 오고 싶은데 못하고 있습니다 팁을 주시면 너무 감사하겠습니다,. 바쁘신 중이라도 알려 주시면 고맙겠습니다. {@ $oContentsModel = getModel('contents');$grade_main = $oContentsModel->getContentsGradeMean($document_srl);}$document_srl 설정하고, 아래는 별점예제입니다. (템플릿 파일)<!--@for($i=0;$i<5;$i++)--><!--@if($i<$grade_main[1])--><img src="./modules/contents/skins/default/form/file/img/starOn.gif" /><!--@else--><img src="./modules/contents/skins/default/form/file/img/starOff.gif" /><!--@end--><!--@end--> {@ $oContentsModel = getModel('contents'); $grade_main = $oContentsModel->getContentsGradeMean($document_srl); } <!--@for($i=0;$i<5;$i++)--> <!--@if($i<$grade_main[1])--><img src="./modules/contents/skins/default/form/file/img/starOn.gif" /><!--@else--><img src="./modules/contents/skins/default/form/file/img/starOff.gif" /><!--@end--> <!--@end-->
이온디
이온디 8년 전
<!--@if($mid=='MID명')--> <!--@if(!$is_logged)--> <?php header('Location: http://eond.com/'); ?> <!--@end--> <!--@end--> <!--@if($mid=='MID명')--> <!--@if(!$is_logged)--> <?php header('Location: http://eond.com/'); ?> <!--@end--> <!--@end-->
이온디
이온디 8년 전
Previewhttps://codepen.io/eond/pen/vpKjRv JS//modal-login $(".ico-spr.profile").on("click",function(){ $(".modal-login").css('display','block'); setTimeout(function() { $(".modal-login").addClass('open'); }, 1) }); //modal-close-area $("body").click(function(e){ … Previewhttps://codepen.io/eond/pen/vpKjRv JS//modal-login $(".ico-spr.profile").on("click",function(){ $(".modal-login").css('display','block'); setTimeout(function() { $(".modal-login").addClass('open'); }, 1) }); //modal-close-area $("body").click(function(e){ if($(".modal-login").hasClass("open")){ // site 라는 특정영역이 열려있을 경우 if(!$(".modal-login").has(e.target).length){ // site에 클릭 이벤트가 발생되어 있는게 없다면 아래 내용을 실행. $('.modal-login').removeClass('open'); setTimeout(function() { $(".modal-login").css('display','none'); }, 50) } } }) //modal-close-btn $(".modal-close").on("click",function(){ $('.modal-login').removeClass('open'); setTimeout(function() { $(".modal-login").css('display','none'); }, 50) });HTML<div class="modal-login"> <div class="modal-box"> <div class="modal-body text-center"> <button class="modal-close">닫기</button> <h2 class="modal-title">Login</h2> 내용 </div> <div class="modal-foot">foot</div> </div> </div>CSS.modal-login{ display: none; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 15px 15px; background: rgba(0, 0, 0, 0.6); opacity: 0; -webkit-transition: opacity 0.15s linear; transition: opacity 0.15s linear; z-index: 1041; padding:400px; width:100%; height:100%; } .modal-box{ position: relative; box-sizing: border-box; margin: 0 auto; width: 600px; max-width: 100%; border-radius: 5px; background: #fff; opacity: 0; -webkit-transform: scale(0.8); transform: scale(0.8); -webkit-transition: 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); transition: 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); -webkit-transition-property: opacity, transform; transition-property: opacity, transform; width:400px; background:#fff; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; /*-webkit-transform: scale(0.8);*/ /*transform: scale(0.8);*/ /*margin-top: auto !important;*/ /*margin-bottom: auto !important;*/ /*position: relative;*/ /*box-sizing: border-box;*/ /*margin: 0 auto;*/ /*max-width: 100%;*/ /*opacity: 1;*/ /*-webkit-transition-property: opacity, transform;*/ /*transition-property: opacity, transform;*/ /*-webkit-transition: 0.3s all;*/ /*transition: 0.3s all;*/ } .modal-login.open{ /*display: -ms-flexbox;*/ /*display: -webkit-flex;*/ /*display: flex;*/ opacity:1; } .modal-login.open .modal-box{ opacity: 1; -webkit-transform: translateY(0); transform: translateY(0); } .modal-body{padding:30px}다른 모달창과 다른점스르륵 열리면서 살짝 심쿵한다-_-; CSS에서 원래 사이즈보다 작게 만든 후에 열리면서 -webkit-transform: translateY(0); transform: translateY(0); 본래 사이즈로 돌아감. 기능소개1. 로그인버튼 클릭하면 스르륵 나타나고 2. 로그인박스 외를 클릭하면 스르륵 닫히고 3. 닫기 버튼 누르면 스르륵 닫히고.. ※css는 기초 코드만 소스에 올립니다. 작업하면서 어려웠던 점1. '스르륵' 열고 닫혀야 한다, display:none, block 일 경우는 스르륵이 안된다. 그래서 setTimeout 으로 인터발을 줬다. 2. 특정 영역을 제외하고 닫혀야 한다. '제이쿼리 특정 영역 제외' 라는 키워드로 검색하면 몇 가지 소스가 있는데.. 레이어의 포지션 문제인지 디스플레이 속성 때문인지 몇가지 소스는 적용이 되도, 그 영역 안에 있는 h2 태그는 제외된다거나, 감싸는 레이어는 적용이 안된다거나 하는 문제가 있었다. 도움 받은 곳 :1. 특정 영역 제외 클릭시 닫히는 소스 (e.target) https://m.blog.naver.com/PostView.nhn?blogId=hsoojy_&logNo=220829360497&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F 2. 닫히는 동작에서 간격 주는 소스 (setTimeout) http://ooz.co.kr/194 3. 영감을 받은 곳 https://getuikit.com/ http://calvinsnax.xyz/ 4. css 중앙정렬 https://webdesign.tutsplus.com/ko/tutorials/the-holy-grail-of-css-centering--cms-22114 추천하는 다른 모달창 소스 1. https://codepen.io/codyhouse/pen/pIrbg?q=modal&limit=all&type=type-pens 2. https://codepen.io/pix3l/pen/ajwcE?q=modal&limit=all&type=type-pens ps. 다음 부터는 그냥 코드펜에서 가져와야지..