feat: initial Jimi Gallery prototype
- Public site (Home/Artists/Exhibitions/News/About/Contact) with EN/KO/JA i18n - Admin panel with login, CRUD, image upload, multilingual editing - Exhibition slider/lightbox view - FastAPI + MongoDB backend, JWT auth - Docker Compose deployment, behind nginx at jimi.yakenator.io
This commit is contained in:
4
backend/.env.example
Normal file
4
backend/.env.example
Normal file
@ -0,0 +1,4 @@
|
||||
ADMIN_PASSWORD=admin
|
||||
JWT_SECRET=change-me-to-a-long-random-string
|
||||
MONGO_URL=mongodb://mongo:27017
|
||||
DB_NAME=jimi_gallery
|
||||
24
backend/Dockerfile
Normal file
24
backend/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
# Backend code
|
||||
COPY backend/ /app/backend/
|
||||
|
||||
# Static frontend baked into the image so the image is self-contained in prod.
|
||||
# Local dev can still override via a volume mount if desired.
|
||||
COPY index.html artist.html artists.html exhibition.html exhibitions.html news.html about.html contact.html /static/
|
||||
COPY assets /static/assets
|
||||
COPY admin /static/admin
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
STATIC_DIR=/static
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
WORKDIR /app/backend
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
37
backend/auth.py
Normal file
37
backend/auth.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Admin auth: bcrypt-less password check + JWT issuance and verification."""
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import jwt
|
||||
from fastapi import Header, HTTPException
|
||||
|
||||
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin")
|
||||
JWT_SECRET = os.environ.get("JWT_SECRET", "dev-secret-change-me")
|
||||
JWT_ALGO = "HS256"
|
||||
TOKEN_TTL_HOURS = int(os.environ.get("JWT_TTL_HOURS", "24"))
|
||||
|
||||
|
||||
def issue_token() -> str:
|
||||
payload = {
|
||||
"sub": "admin",
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=TOKEN_TTL_HOURS),
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO)
|
||||
|
||||
|
||||
def verify_token(token: str) -> bool:
|
||||
try:
|
||||
jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def require_auth(authorization: str = Header(default=None)):
|
||||
if not authorization or not authorization.lower().startswith("bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing bearer token")
|
||||
token = authorization.split(" ", 1)[1].strip()
|
||||
if not verify_token(token):
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
return True
|
||||
24
backend/db.py
Normal file
24
backend/db.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""Mongo connection and collection accessors."""
|
||||
import os
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
MONGO_URL = os.environ.get("MONGO_URL", "mongodb://localhost:27017")
|
||||
DB_NAME = os.environ.get("DB_NAME", "jimi_gallery")
|
||||
|
||||
client = AsyncIOMotorClient(MONGO_URL)
|
||||
db = client[DB_NAME]
|
||||
|
||||
artists = db.artists
|
||||
exhibitions = db.exhibitions
|
||||
news = db.news
|
||||
settings_col = db.settings # single-document collection keyed by _id="singleton"
|
||||
|
||||
|
||||
def strip_id(doc):
|
||||
"""Drop Mongo's _id before returning to the client."""
|
||||
if doc is None:
|
||||
return None
|
||||
doc = dict(doc)
|
||||
doc.pop("_id", None)
|
||||
return doc
|
||||
228
backend/main.py
Normal file
228
backend/main.py
Normal file
@ -0,0 +1,228 @@
|
||||
"""FastAPI app — Jimi Gallery API + static file serving."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Body, Depends, FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from auth import ADMIN_PASSWORD, issue_token, require_auth
|
||||
from db import artists, exhibitions, news, settings_col, strip_id
|
||||
from seed import SEED
|
||||
|
||||
app = FastAPI(title="Jimi Gallery API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# -------------------- bootstrap --------------------
|
||||
|
||||
@app.on_event("startup")
|
||||
async def bootstrap():
|
||||
"""Seed an empty DB on first run so the app has something to show."""
|
||||
if await settings_col.find_one({"_id": "singleton"}) is None:
|
||||
await settings_col.insert_one({"_id": "singleton", **SEED["settings"]})
|
||||
if await artists.estimated_document_count() == 0 and SEED["artists"]:
|
||||
await artists.insert_many([dict(a) for a in SEED["artists"]])
|
||||
if await exhibitions.estimated_document_count() == 0 and SEED["exhibitions"]:
|
||||
await exhibitions.insert_many([dict(e) for e in SEED["exhibitions"]])
|
||||
if await news.estimated_document_count() == 0 and SEED["news"]:
|
||||
await news.insert_many([dict(n) for n in SEED["news"]])
|
||||
|
||||
|
||||
# -------------------- auth --------------------
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def login(body: dict = Body(...)):
|
||||
if body.get("password") != ADMIN_PASSWORD:
|
||||
raise HTTPException(status_code=401, detail="Incorrect password")
|
||||
return {"token": issue_token()}
|
||||
|
||||
|
||||
@app.get("/api/auth/me")
|
||||
async def me(_: bool = Depends(require_auth)):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# -------------------- settings --------------------
|
||||
|
||||
@app.get("/api/settings")
|
||||
async def get_settings():
|
||||
doc = await settings_col.find_one({"_id": "singleton"})
|
||||
if doc is None:
|
||||
doc = {"_id": "singleton", **SEED["settings"]}
|
||||
await settings_col.insert_one(dict(doc))
|
||||
return strip_id(doc)
|
||||
|
||||
|
||||
@app.put("/api/settings")
|
||||
async def put_settings(body: dict = Body(...), _: bool = Depends(require_auth)):
|
||||
body.pop("_id", None)
|
||||
await settings_col.update_one(
|
||||
{"_id": "singleton"}, {"$set": body}, upsert=True
|
||||
)
|
||||
return strip_id(await settings_col.find_one({"_id": "singleton"}))
|
||||
|
||||
|
||||
# -------------------- helpers for collections --------------------
|
||||
|
||||
def _require_id(body):
|
||||
if not body.get("id"):
|
||||
raise HTTPException(status_code=400, detail="id is required")
|
||||
|
||||
|
||||
async def _list(coll):
|
||||
return [strip_id(d) async for d in coll.find({})]
|
||||
|
||||
|
||||
async def _get(coll, id):
|
||||
d = await coll.find_one({"id": id})
|
||||
if d is None:
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
return strip_id(d)
|
||||
|
||||
|
||||
async def _upsert(coll, id, body):
|
||||
body.pop("_id", None)
|
||||
body["id"] = id
|
||||
await coll.update_one({"id": id}, {"$set": body}, upsert=True)
|
||||
return strip_id(await coll.find_one({"id": id}))
|
||||
|
||||
|
||||
async def _delete(coll, id):
|
||||
await coll.delete_one({"id": id})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# -------------------- artists --------------------
|
||||
|
||||
@app.get("/api/artists")
|
||||
async def list_artists():
|
||||
return await _list(artists)
|
||||
|
||||
|
||||
@app.get("/api/artists/{id}")
|
||||
async def get_artist(id: str):
|
||||
return await _get(artists, id)
|
||||
|
||||
|
||||
@app.put("/api/artists/{id}")
|
||||
async def put_artist(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
|
||||
return await _upsert(artists, id, body)
|
||||
|
||||
|
||||
@app.delete("/api/artists/{id}")
|
||||
async def delete_artist(id: str, _: bool = Depends(require_auth)):
|
||||
return await _delete(artists, id)
|
||||
|
||||
|
||||
# -------------------- exhibitions --------------------
|
||||
|
||||
@app.get("/api/exhibitions")
|
||||
async def list_exhibitions():
|
||||
return await _list(exhibitions)
|
||||
|
||||
|
||||
@app.get("/api/exhibitions/{id}")
|
||||
async def get_exhibition(id: str):
|
||||
return await _get(exhibitions, id)
|
||||
|
||||
|
||||
@app.put("/api/exhibitions/{id}")
|
||||
async def put_exhibition(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
|
||||
return await _upsert(exhibitions, id, body)
|
||||
|
||||
|
||||
@app.delete("/api/exhibitions/{id}")
|
||||
async def delete_exhibition(id: str, _: bool = Depends(require_auth)):
|
||||
return await _delete(exhibitions, id)
|
||||
|
||||
|
||||
# -------------------- news --------------------
|
||||
|
||||
@app.get("/api/news")
|
||||
async def list_news():
|
||||
return await _list(news)
|
||||
|
||||
|
||||
@app.get("/api/news/{id}")
|
||||
async def get_news_item(id: str):
|
||||
return await _get(news, id)
|
||||
|
||||
|
||||
@app.put("/api/news/{id}")
|
||||
async def put_news_item(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
|
||||
return await _upsert(news, id, body)
|
||||
|
||||
|
||||
@app.delete("/api/news/{id}")
|
||||
async def delete_news_item(id: str, _: bool = Depends(require_auth)):
|
||||
return await _delete(news, id)
|
||||
|
||||
|
||||
# -------------------- admin: seed / export / import --------------------
|
||||
|
||||
async def _apply_full_set(settings_doc, artists_list, exhibitions_list, news_list):
|
||||
await settings_col.delete_many({})
|
||||
await artists.delete_many({})
|
||||
await exhibitions.delete_many({})
|
||||
await news.delete_many({})
|
||||
|
||||
s = dict(settings_doc)
|
||||
s.pop("_id", None)
|
||||
await settings_col.insert_one({"_id": "singleton", **s})
|
||||
|
||||
if artists_list:
|
||||
await artists.insert_many([dict(a) for a in artists_list])
|
||||
if exhibitions_list:
|
||||
await exhibitions.insert_many([dict(e) for e in exhibitions_list])
|
||||
if news_list:
|
||||
await news.insert_many([dict(n) for n in news_list])
|
||||
|
||||
|
||||
@app.post("/api/admin/seed")
|
||||
async def apply_seed(_: bool = Depends(require_auth)):
|
||||
await _apply_full_set(
|
||||
SEED["settings"], SEED["artists"], SEED["exhibitions"], SEED["news"]
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/admin/export")
|
||||
async def export_all():
|
||||
settings_doc = await settings_col.find_one({"_id": "singleton"}) or {}
|
||||
return {
|
||||
"settings": strip_id(settings_doc) or dict(SEED["settings"]),
|
||||
"artists": [strip_id(d) async for d in artists.find({})],
|
||||
"exhibitions": [strip_id(d) async for d in exhibitions.find({})],
|
||||
"news": [strip_id(d) async for d in news.find({})],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/admin/import")
|
||||
async def import_all(body: dict = Body(...), _: bool = Depends(require_auth)):
|
||||
missing = [
|
||||
k for k in ("settings", "artists", "exhibitions", "news") if k not in body
|
||||
]
|
||||
if missing:
|
||||
raise HTTPException(status_code=400, detail=f"missing keys: {missing}")
|
||||
await _apply_full_set(
|
||||
body["settings"], body["artists"], body["exhibitions"], body["news"]
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# -------------------- static file serving --------------------
|
||||
|
||||
STATIC_DIR = Path(
|
||||
os.environ.get("STATIC_DIR", Path(__file__).resolve().parent.parent)
|
||||
)
|
||||
|
||||
# Mounted last so /api/* routes defined above take precedence.
|
||||
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
|
||||
5
backend/requirements.txt
Normal file
5
backend/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.30.6
|
||||
motor==3.6.0
|
||||
pyjwt==2.9.0
|
||||
python-dotenv==1.0.1
|
||||
376
backend/seed.py
Normal file
376
backend/seed.py
Normal file
@ -0,0 +1,376 @@
|
||||
"""Demo seed data — Python port of the multilingual seed from assets/data.js."""
|
||||
|
||||
SEED = {
|
||||
"settings": {
|
||||
"galleryName": "Jimi Gallery",
|
||||
"tagline": {
|
||||
"en": "Contemporary art. New York.",
|
||||
"ko": "현대미술. 뉴욕.",
|
||||
"ja": "コンテンポラリー・アート。ニューヨーク。",
|
||||
},
|
||||
"address": "145 Grand Street, New York, NY 10013",
|
||||
"phone": "+1 212 555 0180",
|
||||
"email": "info@jimigallery.com",
|
||||
"hours": {
|
||||
"en": "Tuesday – Saturday, 10 – 6",
|
||||
"ko": "화 – 토, 오전 10시 – 오후 6시",
|
||||
"ja": "火 – 土、10時 – 18時",
|
||||
},
|
||||
"instagram": "@jimigallery",
|
||||
"about": {
|
||||
"en": "Jimi Gallery is a contemporary art gallery established in 2014 in downtown New York. The gallery represents an international roster of artists working across painting, sculpture, photography, and installation. Through a program of solo exhibitions, curated group shows, and participation in major art fairs, Jimi Gallery is committed to supporting rigorous, idiosyncratic voices at every stage of their careers.",
|
||||
"ko": "지미 갤러리는 2014년 뉴욕 다운타운에 설립된 현대미술 갤러리입니다. 회화·조각·사진·설치를 아우르는 국제적 작가들을 대표하며, 개인전과 기획 그룹전, 주요 아트페어 참가로 이루어진 프로그램을 통해 경력 단계를 막론하고 엄정하고 독자적인 목소리를 지원하는 데 전념합니다.",
|
||||
"ja": "ジミ・ギャラリーは2014年にニューヨーク・ダウンタウンに設立されたコンテンポラリーアートギャラリーです。絵画、彫刻、写真、インスタレーションを横断する国際的な作家陣を擁し、個展、キュレーションによるグループ展、主要アートフェアへの参加を通じて、キャリアのあらゆる段階で厳格かつ独自な声を支えることに取り組んでいます。",
|
||||
},
|
||||
},
|
||||
"artists": [
|
||||
{
|
||||
"id": "lena-rashid",
|
||||
"name": "Lena Rashid",
|
||||
"born": {"en": "b. 1978, Beirut", "ko": "1978년 베이루트 출생", "ja": "1978年ベイルート生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in Brooklyn, NY",
|
||||
"ko": "뉴욕 브루클린에서 거주 · 작업",
|
||||
"ja": "ニューヨーク・ブルックリンを拠点に活動",
|
||||
},
|
||||
"bio": {
|
||||
"en": "Lena Rashid's paintings move between abstraction and figuration, drawing on the visual logic of textiles, architecture, and 1970s graphic design. Her surfaces, built up over months in thin layers of oil and wax, hold colour the way a wall holds light at dusk.",
|
||||
"ko": "레나 라시드의 회화는 추상과 구상 사이를 오가며, 직물과 건축, 1970년대 그래픽 디자인의 시각 논리에서 자양분을 얻는다. 수개월에 걸쳐 얇은 유채와 왁스 층으로 쌓아 올린 그녀의 화면은, 황혼 녘의 벽이 빛을 품는 방식으로 색을 품는다.",
|
||||
"ja": "レナ・ラシッドの絵画は抽象と具象の間を行き来し、テキスタイル、建築、1970年代のグラフィックデザインの視覚的論理に着想を得ている。数ヶ月にわたり油彩とワックスの薄い層を重ねて築かれる画面は、夕暮れの壁が光を抱くように色を宿している。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Interior (Coral)", "ko": "인테리어 (코랄)", "ja": "インテリア(コーラル)"},
|
||||
"year": "2025",
|
||||
"medium": {"en": "Oil and wax on linen", "ko": "리넨에 유채와 왁스", "ja": "リネンに油彩とワックス"},
|
||||
"dimensions": "180 × 140 cm",
|
||||
"image": "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Lantern Study II", "ko": "랜턴 습작 II", "ja": "ランタン習作 II"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Oil on linen", "ko": "리넨에 유채", "ja": "リネンに油彩"},
|
||||
"dimensions": "120 × 90 cm",
|
||||
"image": "https://images.unsplash.com/photo-1541961017774-22349e4a1262?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Dust Room", "ko": "먼지의 방", "ja": "塵の部屋"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Oil and wax on linen", "ko": "리넨에 유채와 왁스", "ja": "リネンに油彩とワックス"},
|
||||
"dimensions": "200 × 160 cm",
|
||||
"image": "https://images.unsplash.com/photo-1547891654-e66ed7ebb968?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "hiroshi-okada",
|
||||
"name": "Hiroshi Okada",
|
||||
"born": {"en": "b. 1965, Osaka", "ko": "1965년 오사카 출생", "ja": "1965年大阪生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in Kyoto and Los Angeles",
|
||||
"ko": "교토와 로스앤젤레스에서 거주 · 작업",
|
||||
"ja": "京都とロサンゼルスを拠点に活動",
|
||||
},
|
||||
"bio": {
|
||||
"en": "Working primarily in bronze and cast concrete, Hiroshi Okada builds quiet geometric objects that register the passage of hand and time. His practice draws equally on Shinto shrine architecture and mid-century minimalism.",
|
||||
"ko": "히로시 오카다는 주로 청동과 주조 콘크리트로 작업하며, 손과 시간의 흔적을 품은 고요한 기하학적 오브제를 구축한다. 그의 작업은 신토 신사 건축과 미드센추리 미니멀리즘 양쪽으로부터 고르게 자양분을 얻는다.",
|
||||
"ja": "岡田ひろしは主に青銅とキャストコンクリートを用い、手と時間の経過を記録する静謐な幾何学的オブジェを制作する。その実践は神道の社殿建築とミッドセンチュリー・ミニマリズムの双方から等しく影響を受けている。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Stele No. 14", "ko": "스텔레 No. 14", "ja": "ステラ No. 14"},
|
||||
"year": "2025",
|
||||
"medium": {"en": "Patinated bronze", "ko": "파티네이션된 청동", "ja": "パティナ加工の青銅"},
|
||||
"dimensions": "210 × 40 × 40 cm",
|
||||
"image": "https://images.unsplash.com/photo-1578321272176-b7bbc0679853?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Threshold", "ko": "스레숄드", "ja": "スレッショルド"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Cast concrete, steel", "ko": "주조 콘크리트, 강철", "ja": "キャストコンクリート、スチール"},
|
||||
"dimensions": "90 × 120 × 40 cm",
|
||||
"image": "https://images.unsplash.com/photo-1536924430914-91f9e2041b83?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "mara-chen",
|
||||
"name": "Mara Chen",
|
||||
"born": {"en": "b. 1990, Taipei", "ko": "1990년 타이베이 출생", "ja": "1990年台北生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in New York",
|
||||
"ko": "뉴욕에서 거주 · 작업",
|
||||
"ja": "ニューヨークを拠点に活動",
|
||||
},
|
||||
"bio": {
|
||||
"en": "Mara Chen's photographs and video installations treat the city as a layered archive. Her long-term projects document construction sites, shipping ports, and migrant neighbourhoods on the eastern seaboard.",
|
||||
"ko": "마라 첸의 사진과 영상 설치는 도시를 겹겹이 쌓인 아카이브로 다룬다. 그녀의 장기 프로젝트는 미국 동해안의 공사 현장과 항만, 이주민 거주 지역을 기록한다.",
|
||||
"ja": "マラ・チェンの写真と映像インスタレーションは、都市を幾重にも重なったアーカイヴとして扱う。長期プロジェクトでは、アメリカ東海岸の建設現場、港湾、移民コミュニティを記録している。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Red Hook, 4:42am", "ko": "레드 훅, 새벽 4:42", "ja": "レッドフック、午前4:42"},
|
||||
"year": "2025",
|
||||
"medium": {"en": "Archival pigment print", "ko": "아카이벌 피그먼트 프린트", "ja": "アーカイバル顔料プリント"},
|
||||
"dimensions": "110 × 140 cm, ed. of 5",
|
||||
"image": "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Sunset Park (Series)", "ko": "선셋 파크 (연작)", "ja": "サンセット・パーク(シリーズ)"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Archival pigment print", "ko": "아카이벌 피그먼트 프린트", "ja": "アーカイバル顔料プリント"},
|
||||
"dimensions": "80 × 100 cm, ed. of 7",
|
||||
"image": "https://images.unsplash.com/photo-1493514789931-586cb221d7a7?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Container VII", "ko": "컨테이너 VII", "ja": "コンテナ VII"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Archival pigment print", "ko": "아카이벌 피그먼트 프린트", "ja": "アーカイバル顔料プリント"},
|
||||
"dimensions": "100 × 125 cm, ed. of 5",
|
||||
"image": "https://images.unsplash.com/photo-1504276048855-f3d60e69632f?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "david-okafor",
|
||||
"name": "David Okafor",
|
||||
"born": {"en": "b. 1983, Lagos", "ko": "1983년 라고스 출생", "ja": "1983年ラゴス生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in London",
|
||||
"ko": "런던에서 거주 · 작업",
|
||||
"ja": "ロンドンを拠点に活動",
|
||||
},
|
||||
"bio": {
|
||||
"en": "David Okafor's large-scale paintings combine portraiture with densely patterned backgrounds, drawing from Yoruba textiles, 17th-century Dutch interiors, and contemporary street photography.",
|
||||
"ko": "데이비드 오카포르의 대형 회화는 초상과 촘촘히 짜인 패턴 배경을 결합하며, 요루바 직물과 17세기 네덜란드 실내화, 동시대 거리 사진에서 요소를 길어 올린다.",
|
||||
"ja": "デイヴィッド・オカフォーの大型絵画は、肖像と緻密に敷き詰められた模様の背景を組み合わせ、ヨルバのテキスタイル、17世紀オランダの室内画、現代のストリートフォトグラフィーから引用している。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1504593811423-6dd665756598?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Sitter in Green", "ko": "녹색의 시터", "ja": "グリーンのシッター"},
|
||||
"year": "2025",
|
||||
"medium": {"en": "Oil on canvas", "ko": "캔버스에 유채", "ja": "キャンバスに油彩"},
|
||||
"dimensions": "220 × 170 cm",
|
||||
"image": "https://images.unsplash.com/photo-1579762593175-20226054cad0?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"title": {"en": "Brother, Afternoon", "ko": "형제, 오후", "ja": "兄弟、午後"},
|
||||
"year": "2024",
|
||||
"medium": {"en": "Oil on canvas", "ko": "캔버스에 유채", "ja": "キャンバスに油彩"},
|
||||
"dimensions": "190 × 150 cm",
|
||||
"image": "https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "ines-valverde",
|
||||
"name": "Inés Valverde",
|
||||
"born": {"en": "b. 1975, Madrid", "ko": "1975년 마드리드 출생", "ja": "1975年マドリード生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in Madrid",
|
||||
"ko": "마드리드에서 거주 · 작업",
|
||||
"ja": "マドリードを拠点に活動",
|
||||
},
|
||||
"bio": {
|
||||
"en": "Inés Valverde's installations pair found objects with hand-blown glass to meditate on domestic labour, inheritance, and the architecture of Iberian interiors.",
|
||||
"ko": "이네스 발베르데의 설치 작업은 발견된 오브제와 수공 블로운 글라스를 짝 지어, 가사 노동과 상속, 그리고 이베리아 반도 주거 공간의 건축을 사유한다.",
|
||||
"ja": "イネス・バルベルデのインスタレーションは、発見されたオブジェと手吹きガラスを組み合わせ、家事労働、継承、そしてイベリアの室内建築をめぐって思索する。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1544723795-3fb6469f5b39?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Pantry (Herrería)", "ko": "팬트리 (에레리아)", "ja": "パントリー(エレリア)"},
|
||||
"year": "2025",
|
||||
"medium": {"en": "Blown glass, iron, oak", "ko": "블로운 글라스, 철, 오크", "ja": "吹きガラス、鉄、オーク"},
|
||||
"dimensions": "variable",
|
||||
"image": "https://images.unsplash.com/photo-1578926288207-a90a5366759d?w=1200&auto=format&fit=crop&q=70",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "teo-bellini",
|
||||
"name": "Teo Bellini",
|
||||
"born": {"en": "b. 1988, Milan", "ko": "1988년 밀라노 출생", "ja": "1988年ミラノ生まれ"},
|
||||
"lives": {
|
||||
"en": "Lives and works in Berlin",
|
||||
"ko": "베를린에서 거주 · 작업",
|
||||
"ja": "ベルリンを拠点に活동",
|
||||
},
|
||||
"bio": {
|
||||
"en": "Teo Bellini works with discarded electronics, constructing wall-mounted reliefs that hum, glow, and occasionally broadcast fragments of short-wave radio.",
|
||||
"ko": "테오 벨리니는 버려진 전자 부품으로 작업하며, 웅웅거리고 빛나며 때때로 단파 라디오의 파편을 송출하는 벽 부착형 릴리프를 구축한다.",
|
||||
"ja": "テオ・ベッリーニは廃棄された電子機器を素材に、低くうなり、発光し、時折短波ラジオの断片を発信する壁掛けのレリーフを構築する。",
|
||||
},
|
||||
"portrait": "https://images.unsplash.com/photo-1506277886164-e25aa3f4ef7f?w=900&auto=format&fit=crop&q=70",
|
||||
"works": [
|
||||
{
|
||||
"title": {"en": "Channel 88", "ko": "채널 88", "ja": "チャンネル 88"},
|
||||
"year": "2025",
|
||||
"medium": {
|
||||
"en": "Mixed media, LEDs, radio components",
|
||||
"ko": "혼합 매체, LED, 라디오 부품",
|
||||
"ja": "ミクストメディア、LED、ラジオ部品",
|
||||
},
|
||||
"dimensions": "140 × 140 × 12 cm",
|
||||
"image": "https://images.unsplash.com/photo-1517423440428-a5a00ad493e8?w=1200&auto=format&fit=crop&q=70",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"exhibitions": [
|
||||
{
|
||||
"id": "lena-rashid-interiors",
|
||||
"title": {"en": "Interiors", "ko": "인테리어스", "ja": "インテリアーズ"},
|
||||
"artistIds": ["lena-rashid"],
|
||||
"status": "current",
|
||||
"startDate": "2026-04-03",
|
||||
"endDate": "2026-05-16",
|
||||
"venue": "145 Grand Street",
|
||||
"hero": "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=1800&auto=format&fit=crop&q=80",
|
||||
"description": {
|
||||
"en": "Jimi Gallery is pleased to present Interiors, Lena Rashid's third solo exhibition with Jimi Gallery. The show gathers twelve new paintings made over the last eighteen months, alongside a group of smaller works on paper. Rashid's recent canvases turn inward: rooms remembered, surfaces half-seen, the low hum of late afternoon light.",
|
||||
"ko": "지미 갤러리는 레나 라시드의 세 번째 지미 갤러리 개인전 «인테리어스»를 선보이게 되어 기쁩니다. 이번 전시는 지난 18개월 동안 제작한 열두 점의 신작 회화와 소규모 종이 작업 일군을 함께 소개합니다. 라시드의 최근 화면은 안쪽을 향합니다 — 기억 속의 방, 절반쯤 보이는 표면, 늦은 오후 빛의 낮은 울림.",
|
||||
"ja": "ジミ・ギャラリーは、レナ・ラシッドの当ギャラリーにおける3度目の個展「インテリアーズ」を開催いたします。本展では、過去18ヶ月の間に制作された新作絵画12点と、より小さな紙作品群を併せて紹介します。ラシッドの近作キャンバスは内面へと向かいます——記憶のなかの部屋、半ば見える表面、午後遅くの光の低い響き。",
|
||||
},
|
||||
"press": {
|
||||
"en": "Accompanied by a new monograph published by Jimi Gallery Editions, with an essay by the poet Kei Miller.",
|
||||
"ko": "지미 갤러리 에디션즈에서 발간한 신간 모노그래프 동반 출간. 시인 케이 밀러의 에세이 수록.",
|
||||
"ja": "ジミ・ギャラリー・エディションズより新モノグラフを刊行、詩人ケイ・ミラーによるエッセイを収録。",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "okafor-sitter",
|
||||
"title": {"en": "The Sitter", "ko": "시터", "ja": "ザ・シッター"},
|
||||
"artistIds": ["david-okafor"],
|
||||
"status": "upcoming",
|
||||
"startDate": "2026-05-28",
|
||||
"endDate": "2026-07-11",
|
||||
"venue": "145 Grand Street",
|
||||
"hero": "https://images.unsplash.com/photo-1579762593175-20226054cad0?w=1800&auto=format&fit=crop&q=80",
|
||||
"description": {
|
||||
"en": "David Okafor's first exhibition with Jimi Gallery brings together six monumental portraits painted between 2024 and 2026. The works extend Okafor's ongoing inquiry into the history of the posed figure, routing European portrait traditions through the specific rooms, textiles, and light of the Lagos-London corridor.",
|
||||
"ko": "데이비드 오카포르의 지미 갤러리 첫 개인전은 2024년부터 2026년 사이에 그려진 여섯 점의 기념비적 초상화를 한자리에 모읍니다. 이 작품들은 포즈를 취한 인물의 역사에 대한 오카포르의 지속적 탐구를 확장하며, 유럽 초상화 전통을 라고스–런던 축의 구체적인 방, 직물, 빛을 통해 다시 짜냅니다.",
|
||||
"ja": "デイヴィッド・オカフォーのジミ・ギャラリー初個展では、2024年から2026年にかけて描かれた6点の大型ポートレイトを一堂に展示します。本作群はポーズをとる人物像の歴史への持続的探究をさらに押し広げ、ヨーロッパのポートレイト伝統を、ラゴス—ロンドン回廊の具体的な部屋、テキスタイル、光を通じて再編しています。",
|
||||
},
|
||||
"press": "",
|
||||
},
|
||||
{
|
||||
"id": "threshold-group",
|
||||
"title": {"en": "Threshold", "ko": "스레숄드", "ja": "スレッショルド"},
|
||||
"artistIds": ["hiroshi-okada", "ines-valverde", "teo-bellini"],
|
||||
"status": "upcoming",
|
||||
"startDate": "2026-07-23",
|
||||
"endDate": "2026-08-30",
|
||||
"venue": "145 Grand Street",
|
||||
"hero": "https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=1800&auto=format&fit=crop&q=80",
|
||||
"description": {
|
||||
"en": "A three-person summer exhibition bringing together new sculpture and installation by Hiroshi Okada, Inés Valverde, and Teo Bellini. Across bronze, glass, concrete and electronics, the works gathered here share an interest in passages: between rooms, between signals, between one material state and the next.",
|
||||
"ko": "히로시 오카다, 이네스 발베르데, 테오 벨리니의 신작 조각과 설치를 한자리에 모은 3인 여름 전시. 청동, 유리, 콘크리트, 전자 부품을 가로지르며, 이곳에 모인 작품들은 통로에 대한 관심을 공유합니다 — 방과 방 사이, 신호와 신호 사이, 한 물질적 상태에서 다음 상태로의 이행.",
|
||||
"ja": "岡田ひろし、イネス・バルベルデ、テオ・ベッリーニの新作彫刻・インスタレーションを集めた夏の3人展。青銅、ガラス、コンクリート、電子機器を横断しながら、本展の作品群は「通路」への関心を共有しています——部屋と部屋の間、信号と信号の間、ある物質的状態から次の状態への移行。",
|
||||
},
|
||||
"press": "",
|
||||
},
|
||||
{
|
||||
"id": "mara-chen-eastern-seaboard",
|
||||
"title": {"en": "Eastern Seaboard", "ko": "이스턴 시보드", "ja": "イースタン・シーボード"},
|
||||
"artistIds": ["mara-chen"],
|
||||
"status": "past",
|
||||
"startDate": "2026-02-07",
|
||||
"endDate": "2026-03-22",
|
||||
"venue": "145 Grand Street",
|
||||
"hero": "https://images.unsplash.com/photo-1504276048855-f3d60e69632f?w=1800&auto=format&fit=crop&q=80",
|
||||
"description": {
|
||||
"en": "A solo exhibition of new photographs and a single-channel video by Mara Chen, drawn from four years of fieldwork along the ports, rail yards, and migrant neighbourhoods of the eastern United States.",
|
||||
"ko": "마라 첸의 신작 사진과 싱글 채널 영상으로 구성된 개인전. 미국 동부의 항구, 철도 조차장, 이주민 거주 지역을 4년간 현장 조사한 결과에서 길어 올린 작업들을 소개합니다.",
|
||||
"ja": "マラ・チェンによる新作写真群とシングルチャンネル映像からなる個展。アメリカ東部の港湾、操車場、移民コミュニティを4年にわたりフィールドワークした成果から構成されます。",
|
||||
},
|
||||
"press": "",
|
||||
},
|
||||
{
|
||||
"id": "okada-survey",
|
||||
"title": {
|
||||
"en": "Hiroshi Okada: Fifteen Years",
|
||||
"ko": "히로시 오카다: 15년",
|
||||
"ja": "岡田ひろし:15年",
|
||||
},
|
||||
"artistIds": ["hiroshi-okada"],
|
||||
"status": "past",
|
||||
"startDate": "2025-10-04",
|
||||
"endDate": "2025-12-20",
|
||||
"venue": "145 Grand Street",
|
||||
"hero": "https://images.unsplash.com/photo-1578321272176-b7bbc0679853?w=1800&auto=format&fit=crop&q=80",
|
||||
"description": {
|
||||
"en": "A fifteen-year survey of Hiroshi Okada's sculpture, organised in collaboration with the artist's studio in Kyoto.",
|
||||
"ko": "히로시 오카다의 조각 15년을 조망하는 서베이 전시. 교토의 작가 스튜디오와 공동 기획.",
|
||||
"ja": "岡田ひろしの彫刻の15年を概観するサーベイ展。京都の作家スタジオと共同企画。",
|
||||
},
|
||||
"press": "",
|
||||
},
|
||||
],
|
||||
"news": [
|
||||
{
|
||||
"id": "rashid-kei-miller",
|
||||
"title": {
|
||||
"en": "Kei Miller writes on Lena Rashid's Interiors",
|
||||
"ko": "케이 밀러, 레나 라시드 «인테리어스»에 부쳐",
|
||||
"ja": "ケイ・ミラー、レナ・ラシッド「インテリアーズ」によせて",
|
||||
},
|
||||
"date": "2026-04-15",
|
||||
"excerpt": {
|
||||
"en": "A new essay by the poet Kei Miller accompanies Rashid's current exhibition, published in the Jimi Gallery Editions monograph.",
|
||||
"ko": "시인 케이 밀러의 신작 에세이가 라시드의 현재 전시와 함께 지미 갤러리 에디션즈 모노그래프에 수록됩니다.",
|
||||
"ja": "詩人ケイ・ミラーによる新しいエッセイが、ラシッドの現在の展覧会に寄せて、ジミ・ギャラリー・エディションズのモノグラフに掲載されます。",
|
||||
},
|
||||
"body": {
|
||||
"en": "Kei Miller's essay, 'A room is a kind of sentence,' appears in the monograph published on the occasion of Lena Rashid's exhibition Interiors. Copies are available at the gallery and through Jimi Gallery Editions.",
|
||||
"ko": "케이 밀러의 에세이 «방은 일종의 문장이다»가 레나 라시드의 전시 «인테리어스» 개최에 맞춰 발간된 모노그래프에 실립니다. 갤러리 및 지미 갤러리 에디션즈를 통해 구입하실 수 있습니다.",
|
||||
"ja": "ケイ・ミラーのエッセイ「部屋とはひとつの文のようなもの」が、レナ・ラシッドの展覧会「インテリアーズ」開催に合わせて刊行されるモノグラフに掲載されます。ギャラリーおよびジミ・ギャラリー・エディションズにてお求めいただけます。",
|
||||
},
|
||||
"image": "https://images.unsplash.com/photo-1549490349-8643362247b5?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"id": "basel-2026",
|
||||
"title": {
|
||||
"en": "Jimi Gallery at Art Basel 2026",
|
||||
"ko": "지미 갤러리, 아트 바젤 2026 참가",
|
||||
"ja": "ジミ・ギャラリー、アート・バーゼル2026に参加",
|
||||
},
|
||||
"date": "2026-06-10",
|
||||
"excerpt": {
|
||||
"en": "The gallery will present a two-artist booth of works by David Okafor and Inés Valverde at Art Basel, Hall 2.1, booth C14.",
|
||||
"ko": "데이비드 오카포르와 이네스 발베르데의 작품을 선보이는 2인 부스로 아트 바젤에 참가합니다. 홀 2.1, 부스 C14.",
|
||||
"ja": "デイヴィッド・オカフォーとイネス・バルベルデの作品によるデュオブースでアート・バーゼルに参加します。ホール2.1、ブースC14。",
|
||||
},
|
||||
"body": {
|
||||
"en": "Jimi Gallery is pleased to announce its participation in Art Basel 2026 with a two-artist booth of new works by David Okafor and Inés Valverde. The booth will be located in Hall 2.1, C14. Preview days are 16–17 June, with public days running 18–21 June.",
|
||||
"ko": "지미 갤러리는 아트 바젤 2026에 데이비드 오카포르와 이네스 발베르데의 신작으로 구성된 2인 부스로 참가함을 알려드립니다. 부스는 홀 2.1, C14에 위치합니다. 프리뷰 데이는 6월 16–17일, 일반 공개는 6월 18–21일입니다.",
|
||||
"ja": "ジミ・ギャラリーはデイヴィッド・オカフォーとイネス・バルベルデの新作によるデュオブースで、アート・バーゼル2026に参加いたします。ブースはホール2.1、C14に設置されます。プレビュー日は6月16–17日、一般公開日は6月18–21日です。",
|
||||
},
|
||||
"image": "https://images.unsplash.com/photo-1547891654-e66ed7ebb968?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
{
|
||||
"id": "chen-moma",
|
||||
"title": {
|
||||
"en": "Mara Chen acquired by MoMA",
|
||||
"ko": "마라 첸, 뉴욕 현대미술관 소장",
|
||||
"ja": "マラ・チェン、ニューヨーク近代美術館に収蔵",
|
||||
},
|
||||
"date": "2026-03-02",
|
||||
"excerpt": {
|
||||
"en": "Three works from the series 'Eastern Seaboard' have entered the permanent collection of the Museum of Modern Art.",
|
||||
"ko": "«이스턴 시보드» 연작 중 세 점이 뉴욕 현대미술관 영구 소장품으로 편입되었습니다.",
|
||||
"ja": "シリーズ「イースタン・シーボード」より3点が、ニューヨーク近代美術館の永久コレクションに加わりました。",
|
||||
},
|
||||
"body": {
|
||||
"en": "The gallery is delighted to announce that three photographs from Mara Chen's 'Eastern Seaboard' series have been acquired by the Museum of Modern Art for its permanent collection. The works were on view at Jimi Gallery earlier this spring.",
|
||||
"ko": "지미 갤러리는 마라 첸의 «이스턴 시보드» 연작 중 세 점의 사진이 뉴욕 현대미술관 영구 소장품으로 수집되었음을 알리게 되어 기쁩니다. 해당 작품들은 올해 초봄 지미 갤러리에서 선보였습니다.",
|
||||
"ja": "マラ・チェン「イースタン・シーボード」シリーズより写真3点が、ニューヨーク近代美術館の永久コレクションに収蔵されましたことをお知らせいたします。本作品は今春、ジミ・ギャラリーにて展示されていました。",
|
||||
},
|
||||
"image": "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200&auto=format&fit=crop&q=70",
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user