iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Movie night decided the way a lot of households already do it: everyone writes a
title on a slip of paper, the slips go in a bowl, and somebody pulls one out.
Movie Bowl is that bowl, shared and online. Everyone adds the titles they want to
watch, and when it is time to pick, the app draws one.README 전문 보기 ↗Live app: moviebowl.app
Movie night decided the way a lot of households already do it: everyone writes a title on a slip of paper, the slips go in a bowl, and somebody pulls one out.
Movie Bowl is that bowl, shared and online. Everyone adds the titles they want to watch, and when it is time to pick, the app draws one.
A real bowl of slips gives the odds to the paper: someone who wrote twenty-five slips is twenty-five times likelier to win than someone who wrote one. The default draw here deliberately does not work that way. It picks a person first, evenly, from everyone who still has a title in the running — then a title from that person. Adding more movies gives you more ways to be chosen, not a better chance of being the one chosen.
A bowl's owner can switch it to the flat version, where the odds do follow the count, or to a rotation that gives the next turn to whoever has waited longest.
Bowls
Make [bowl] home in the
bowl picker on the dashboard header — a home bowl can be moved but never
unset, so nothing toggles it off. The bowl directory marks which one is home
and offers no control to change it.Movies
/quick-add).The draw
/solo-draw), from the bowls you
belong to.History
/watch-list that survives leaving or deleting
a bowl, including manually added entries.TV
/tv with D-pad/remote spatial navigation, a bowl
picker, and a theater-mode trailer pre-roll before the pick is revealed.Use my phone's settings, which hands it all back at once.New to the project? Start with ONBOARDING.md: it covers a first week, a local backend of your own, and what not to touch yet. The steps below are the full production configuration.
Provider title links are optional and off until configured; production has them enabled. See Provider title links for activation and cache maintenance.
nvm install
nvm use
npm ci
.env with:VITE_SUPABASE_URL=...
VITE_SUPABASE_ANON_KEY=...
TMDB_READ_ACCESS_TOKEN=...
SUPABASE_URL=...
SUPABASE_SECRET_KEY=...
TV_PAIRING_RATE_LIMIT_SECRET=...
CRON_SECRET=...
APP_BASE_URL=https://moviebowl.app
RESEND_API_KEY=...
INVITE_EMAIL_FROM="Movie Bowl <[email protected]>"
SUPABASE_URL should match VITE_SUPABASE_URL.
For full local behavior, including /api/* routes:
npm run dev:api
If you only need frontend-only iteration, you can still use:
npm run dev
npm run dev does not serve the serverless functions, so /api/* requests 404
— TMDB search, invite email, and public add links need npm run dev:api.
Run it through the script rather than a bare vercel dev. The CLI is pinned as
a devDependency here, and a globally installed one can be old enough to reject
this repo's vercel.json outright: has on a headers rule postdates CLI 21,
and that rule is what scopes the no-cache header to HTML documents, so it cannot
simply be removed.
npm run dev - start dev server (frontend only)npm run dev:api - dev server including the /api/* serverless routesnpm run build - production buildnpm run preview - preview production buildnpm run lint - run ESLintnpm run test - start Vitest in watch modenpm run test:run - run tests oncenpm run test:failures - name what failed in the last test:runnpm run test:counts - check the last runs lost no test against mainnpm run test:coverage - run tests with coveragenpm run test:e2e - run the local Playwright smoke suitenode scripts/refresh-provider-logos.mjs - regenerate src/utils/providerLogos.jssrc/
main.jsx AuthProvider -> App
App.jsx router, route guards, invite acceptance
screens/ one screen per route, lazily imported
components/ shared UI
hooks/ stateful data layer (useBowl, useAuth, useAutosave, ...)
lib/ external-service clients (supabase, tmdbApi, streamingProviders)
utils/ pure logic (draw selection, filters, formatting, storage)
tv/ self-contained TV experience: own screens, hooks, css
index.css design tokens + shared component classes
api/ Vercel serverless functions (not part of the Vite build)
_lib/ server-only helpers (supabaseAdmin, tmdb)
supabase/
migrations/ source of truth for schema, RLS, functions
tests/ pgTAP tests for security-sensitive migrations
rollback/ staged reverts, kept out of migrations/ on purpose
tv-android/ Google TV WebView shell around /tv; now in an owner-only
Google Play internal test. Has its own README
android-mobile/ disposable Android App Actions probe. It does not contain
the product app
output/designs/ design specs and roadmaps for shipped + planned features
/ (resolves your home bowl, or /bowls when you have none), /bowls,
/bowl/:bowlId, /bowl/:bowlId/settings, /settings, /watch-list,
/invites, /about, /login, /accept-invite/:token, /add-to-bowl/:token,
/activate-tv, /voice-probe/privacy, /tv/*.
Everything except /login, /about, /accept-invite/:token,
/add-to-bowl/:token, /activate-tv, and /voice-probe/privacy requires a
signed-in user.
/bowls never redirects. / computes a destination; /bowls computes nothing
and always renders, which is what makes it the recovery surface every failed
resolution links back to.
src/App.jsxsrc/hooks/useBowl.jssrc/utils/drawSelection.jssrc/utils/selectDrawCandidate.jssrc/utils/drawBuckets.jssrc/hooks/useUserStreamingServices.jssrc/screens/BowlDashboard.jsxsrc/screens/BowlSettings.jsxsrc/screens/UserSettings.jsxsrc/screens/WatchListPage.jsxsrc/tv/TvApp.jsxapi/invites/send.jsapi/add-links/*For how the code is put together and the conventions to follow when changing it,
see CLAUDE.md. For reliability guardrails and the release smoke checklist, see
STABILITY.md. TODO.md is the live backlog.
src/utils/streamingServices.js.src/lib/streamingProviders.jsapi/tmdb/* so the TMDB key stays server-sideVITE_SUPABASE_URLVITE_SUPABASE_ANON_KEYThese are visible in the browser bundle by design.
TMDB_READ_ACCESS_TOKENSUPABASE_URLSUPABASE_SECRET_KEY (preferred) or SUPABASE_SERVICE_ROLE_KEY (legacy compatibility)TV_PAIRING_RATE_LIMIT_SECRETCRON_SECRETFILTER_METADATA_DAILY_MAX_TITLES (optional emergency override; defaults to 300)APP_BASE_URLRESEND_API_KEYINVITE_EMAIL_FROMFEEDBACK_EMAIL_TO (optional; where feedback is mailed, defaulting to the
support address)WATCHMODE_API_KEY, PROVIDER_LINKS_ENABLED, PROVIDER_LINKS_MONTHLY_BUDGET
— optional, and off unless set; production sets them. See
Provider title links.MOVIE_OF_THE_NIGHT_API_KEY — optional server secret for US departure notices;
no departure lookups run without it. MOVIE_OF_THE_NIGHT_REQUEST_BUDGET may
lower the rolling 32-UTC-date cap: 900 in Production, 100 in nonproduction.
Nonproduction cannot exceed 100; projects sharing a key must keep their
combined budgets within its allowance.
Set the secret separately in each deployment environment that should use it.GROQ_API_KEY, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_TOKEN — optional;
described search is off without them. See
Described search.EMAIL_DAILY_WARN_THRESHOLD (optional; defaults to 80). See
Usage counters.Do not prefix server-only values with VITE_. The Supabase secret/service-role
key bypasses RLS, so every route that uses it must do its own authorization.
TV_PAIRING_RATE_LIMIT_SECRET is a random server-only value of at least 32
characters. It HMAC-pseudonymizes client addresses and user IDs before the
pairing rate limiter writes its short-lived counters; raw identifiers are never
stored in the counter table.
CRON_SECRET protects the once-daily Vercel filter-metadata refresh. Vercel
sends it as a bearer token when invoking the configured cron route.
public.service_usage_counters records how much metered vendor quota the app
spends, one row per day per metric, so a cost or capacity question can be
answered with a number. Writes go through record_service_usage; no role
reaches the table directly, the service role included. Read it as the project
owner in the SQL editor:
-- The last fortnight, by metric.
select usage_date, metric, event_count
from public.service_usage_counters
where usage_date >= current_date - 14
order by usage_date desc, metric;
-- This month's totals.
select metric, sum(event_count) as events
from public.service_usage_counters
where usage_date >= date_trunc('month', current_date)::date
group by metric;
Counters are daily rather than monthly on purpose. The mail cap that threatens this app is a daily one, and a monthly total cannot see a single day's burst inside it; a month is the sum of its days, so daily rows answer both questions.
The meter never refuses the action it measures, and every failure to record is
swallowed with a logged error. Budgets that must actually hold belong in a
function like begin_title_provider_link_fetch, which reserves before HTTP.
Two metrics are recorded today:
invite_email — one per invite the send route attempted, counted whether or
not the vendor accepted it, because a rejected send still spent the quota.
Crossing EMAIL_DAILY_WARN_THRESHOLD logs a warning rather than refusing;
the vendor enforces its own ceiling, and stopping early would turn "nearly
out" into "nobody can be invited."tmdb_request — recorded once per daily refresh run with the number of
titles claimed, rather than once per title. The run works inside a
60-second budget, and 300 extra round trips would make the meter a cost of
its own.Two things this cannot see. Magic-link authentication mail is sent by
Supabase's SMTP after a client call that never reaches these functions, so it is
absent from invite_email; the mail vendor's own dashboard is authoritative for
total mail. The user-facing api/tmdb/* proxies are also unmetered, which keeps
the service-role client out of routes that do not otherwise need it — Vercel's
function metrics cover their volume.
Adding a metric means extending the check constraint on
service_usage_counters.metric. The constraint is deliberate: a free text
column turns one typo into a phantom metric that reads as zero usage forever.
https://moviebowl.appmoviebowl.app as the primary production domain in VercelSite URL should be https://moviebowl.appAllowed Redirect URLs should include:
https://moviebowl.apphttp://localhost:3000http://localhost:5173mail.moviebowl.appmail.moviebowl.app in ResendMovie Bowl <[email protected]>/activate-tv for QR pairing while phones and ordinary web
browsers retain the magic-link flowPOST /api/tv-pairing/start,
POST /api/tv-pairing/approve, and POST /api/tv-pairing/poll20260905120000_rate_limit_tv_pairing.sql and set
TV_PAIRING_RATE_LIMIT_SECRET before deploying the matching server routes.Movie Bowl <[email protected]>bowl_invitesPOST /api/invites/sendAPP_BASE_URL/add-to-bowl/:tokenGET /api/add-links/:tokenPOST /api/add-links/consumeGET /api/cron/refresh-filter-metadata once per day.tmdb_filter_metadata_refresh_runs.POST /api/tmdb/movie/warm-filter-metadata.Movie data, provider availability, and provider logos come from TMDB, and using them carries conditions the About page already satisfies:
Two consequences worth knowing before changing anything here:
src/lib/streamingProviders.js
caches for minutes, so it is unaffected. The details copied onto saved rows
are refreshed or cleared by the daily run (see "Filter metadata refresh"). src/utils/providerLogos.js is the
exception: it is generated, stamped with the day it was made, and has to be
regenerated with node scripts/refresh-provider-logos.mjs before that date
ages out. A lapse degrades to service names rather than breaking, which is
exactly why the deadline is written down in TODO.md.Tables the app touches:
profilesbowlsbowl_membersbowl_moviesbowl_invitesbowl_draw_permissionstv_pairing_requests (server-only; no direct client access)tv_pairing_rate_limits (server-only pseudonymous fixed-window counters)tmdb_filter_metadata (server-maintained; read through a membership RPC)tmdb_filter_metadata_refresh_runs (private 90-day cron run history)bowl_add_linksuser_bowl_defaults — the account's home bowlbowl_draw_events — immutable bowl-side record of each drawuser_watch_events — per-participant personal historybowl_movie_queue — legacy compatibility table; not written toprofiles.display_name is the only shared member identity. It is optional,
non-unique, and limited to 40 characters; shared app and TV surfaces use a
neutral Member XXXX fallback and never derive a name from an email address.
Email remains visible only to its owner in Account settings and where an invite
address is operationally required.
Account deletion is initiated from Settings and completed by the trusted
/api/account/delete route. Owners must transfer or delete every bowl first.
Cleanup removes suggestions, memberships, preferences, invitation data, and
personal watch history; completed bowl history remains with its contributor
anonymized as Former member before the Auth user is hard-deleted.
A draw writes one bowl_draw_events row plus one user_watch_events row per
participant, which is why personal history survives leaving or deleting a bowl.
Returning a movie to the bowl sets returned_at on the draw event rather than
deleting it, and return_bowl_draw_to_bowl refuses more than two hours after
the draw. Inside that window the return also deletes the personal history rows
the draw created, because a return means the group did not watch it. Every
surface reads active draws only.
Custom (non-TMDB) movies carry a negative synthetic tmdb_id, so anything that
calls TMDB must filter for Number(tmdb_id) > 0 first.
RPCs the client calls (preferred over multi-statement client writes, because they are the atomic and permission-checked path):
get_my_bowls_with_counts, get_my_bowl_context, set_my_default_bowl,
get_bowl_profile_directory, get_bowl_filter_metadata,
get_my_invite_sender_directory, accept_bowl_invite, create_bowl_invites,
revoke_bowl_invite, draw_bowl_movie, draw_bowl_movie_by_rotation,
return_bowl_draw_to_bowl, remove_bowl_draw_from_history,
save_bowl_draw_access, save_bowl_draw_method, create_owned_bowl,
transfer_owned_bowl, delete_owned_bowl, set_own_bowl_movie_pin,
update_own_bowl_movie_note, remove_own_bowl_movie, consume_bowl_add_link,
install_bowl_starter_pack, remove_bowl_starter_pack,
claim_bowl_starter_pack_movie, record_solo_draw, undo_solo_draw,
create_manual_watch_event, update_user_watch_event,
update_own_watch_event_note, delete_user_watch_event.
delete_account_data_for_user is intentionally absent from that client list:
only the service role may execute it, immediately before the server deletes the
matching Supabase Auth user.
The interface says home bowl where the database says default. That drift
is deliberate — user_bowl_defaults, get_my_bowl_context and
set_my_default_bowl are deployed objects and are not renamed to match the
word on screen.
Store all Supabase schema and RLS/policy changes in supabase/migrations/ with
a timestamped filename — never make dashboard-only edits.
Quick workflow:
supabase login
supabase link --project-ref YOUR_PROJECT_REF
supabase db pull a baseline migration; the
pre-migration schema is supabase/baseline/, kept out of migrations/ on
purpose):supabase migration new short_description
supabase db push
For permission-sensitive changes, add a pgTAP test in supabase/tests/ and a
revert in supabase/rollback/. See supabase/README.md for details.
Tests live in __tests__/ next to the code they cover. Current coverage
includes:
src/screens/__tests__/BowlDashboard.*.test.jsx)src/utils/__tests__/selectDrawCandidate.test.js)src/hooks/__tests__/useBowl.test.js)src/tv/__tests__/)supabase/tests/)Run all tests:
npm run test:run
The Playwright smoke suite exercises the release-critical flows in a real, headless Chromium browser at desktop and mobile sizes. It supplies an isolated in-memory Supabase/API boundary, so it never needs production credentials or data and does not require the serverless routes. Install Chromium once, then run it with:
npx playwright install chromium
npm run test:e2e
TV-specific checks run only in the desktop project. Failures retain a trace,
screenshot, and video in test-results/; the HTML report is written to
playwright-report/. Both directories are gitignored.
A clean checkout is expected to be fully green, with lint reporting zero
warnings. Run npm run test:run, npm run test:e2e, and npm run build before
committing anything non-trivial. npm run test:counts compares each suite's
last run with the commit the branch grew from, so a lost test is visible rather
than silent.
The end-to-end suite is in that list rather than in a release checklist because leaving it out did not hold: it was red on a clean checkout through three separate merges, every failure a selector still describing an interface that had been deliberately replaced. A suite that is red by default cannot report a regression.
A search that reads like a description ("space movie where Matt Damon is stranded") rather than a title or a name goes to a small language model, which turns it into TMDB terms and may name titles it recognizes from a quote or a scene; TMDB's discover endpoint finds the movies, and a named title is shown only when TMDB has a movie by exactly that name. A search that finds a title or a person never reaches the model; one of four or more letters that finds nothing does, since a misspelled name is what it reads well. Both providers are free tiers, tried in order:
GROQ_API_KEY=... # console.groq.com, free plan
CLOUDFLARE_ACCOUNT_ID=... # Workers AI, free daily allocation
CLOUDFLARE_AI_TOKEN=... # API token with Workers AI read permission
Either one is enough. GROQ_MODEL, GROQ_REASONING_EFFORT and
CLOUDFLARE_AI_MODEL override the models without a code deploy. With neither, or when both are over their daily limit or
down, a described search shows ordinary title results with a struck-through
sparkle and "Smart search is resting." It is offered to signed-in people only,
so a public add link never spends the quota. Do not use Gemini's free tier
here: Google may review what is sent, and its terms bar serving EU or UK users
on it. See output/designs/described-search.md.
Phone and TV draw results can open the chosen service's title page instead of its search page. The saved service order and the phone's opt-in setting are unchanged. Only subscription/free sources open as a service. Rental and purchase links are filed under the store that sells them. Rental links alone feed the rent button a drawn movie offers when none of your services carry it; purchase links are cached but never used; the TV offers Apple TV and Prime Video, the two stores the Google TV app can hand off to. A title link does not guarantee automatic playback or bypass a subscription.
To activate. Production is already activated, so these steps are for a new
environment, such as a local .env or a fresh Vercel project:
Apply supabase/migrations/20260830120000_add_title_provider_links.sql before
deploying the server code. The new tables and maintenance RPCs are private
to the service role; the lookup route verifies both the bearer token and
access to the requested bowl/title.
Create a Watchmode key with US access. The intended free, non-commercial
setup has no payment card attached. Store these server-only values in
local .env and the relevant Vercel environment (never use VITE_):
WATCHMODE_API_KEY=...
PROVIDER_LINKS_ENABLED=false
PROVIDER_LINKS_MONTHLY_BUDGET=500
Verify the existing daily /api/cron/refresh-filter-metadata job is running,
then set PROVIDER_LINKS_ENABLED=true and restart/redeploy. Use
npm run dev:api locally because Vite alone does not serve the lookup API.
Lookups happen after a signed-in member adds a TMDB movie and during a draw's animation. Public add links, custom titles, and detail/browse screens do not spend requests. A ten-minute browser cache deduplicates calls, and Supabase caches results for up to 30 days. Failures back off from five minutes to one day. The monthly budget is reserved atomically before HTTP and counts requests, including failed requests; Watchmode currently charges two credits per TMDB lookup, so the default caps this integration at 1,000 credits per UTC month. Requests made outside this integration are not counted by this budget.
The free plan requires attribution and deletion/refresh within 30 days. Direct
links display Watchmode attribution. The existing daily job deletes rows at
29 days (no vendor requests), even when lookups are disabled; lookups and the
browser cache also refuse expired data. Monitor failures of that daily job.
If the Watchmode account is cancelled, disable lookups and delete the stored
vendor data using delete from public.title_provider_links; as an administrator.
Missing key, disabled flag, lookup errors, and quota exhaustion all retain the
existing search fallback. The immediate rollback is to disable the flag and
redeploy (open clients can retain a cached result for ten minutes). The schema
rollback is in supabase/rollback/20260830120000_remove_title_provider_links.sql;
revert the server's cleanup call before removing its RPC.
The account-deletion, provider-lookup, filter-metadata warmup, starter-pack
candidate and starter-pack photo URLs share api/movie-cache.js through
explicit rewrites in vercel.json. Their private
handlers live in api/_lib/; keeping them together holds the deployment at
Vercel Hobby's 12-function limit without changing the client URLs or
authentication checks.
Native iOS/Android URLs are optional: Watchmode's free plan does not include them. They are retained when supplied, but this phase uses web URLs only. Vendor details verified against Watchmode's API documentation, plan information, and caching terms on August 30, 2026.